In C# zijn events een manier om het publish-subscribe pattern te implementeren. Een event is een mechanisme waarmee een object (de publisher) andere objecten (de subscribers) kan informeren wanneer een bepaalde gebeurtenis plaatsvindt. Dit zorgt voor een losse koppeling tussen componenten, omdat de publisher niet hoeft te weten welke subscribers erop reageren.
Events worden vaak gebruikt in GUI-toepassingen (zoals klikken op een knop), achtergrondprocessen (zoals het voltooien van een download), en in event-driven architecturen binnen software. Ze worden meestal in combinatie met delegates gebruikt om de bijbehorende functies aan te roepen wanneer het event wordt getriggerd.
Een delegate in C# is een type dat een verwijzing naar een methode kan opslaan en doorgeven. Het werkt als een soort functiepointer, maar dan met extra typeveiligheid. Delegates maken het mogelijk om methoden dynamisch aan elkaar te koppelen en door te geven als argumenten, zonder dat de aanroepende code hoeft te weten welke specifieke methode wordt uitgevoerd.
Ze worden vaak gebruikt bij events en callbacks, waarbij een bepaalde actie pas wordt uitgevoerd wanneer een gebeurtenis plaatsvindt. Hierdoor bevorderen delegates een losse koppeling tussen componenten en maken ze flexibelere en herbruikbare code mogelijk.
Casus
Een slimme deurbel is een voorbeeld van het publish-subscribe patroon. Wanneer iemand op de deurbel drukt (event), stuurt de deurbel (publisher) een notificatie naar alle gekoppelde telefoons (subscribers).
Op dezelfde manier werken push-notificaties op je telefoon. Een app kan zich registreren voor bepaalde updates, zoals breaking news of aanbiedingen (subscriber). Zodra er een nieuwe gebeurtenis plaatsvindt (event), wordt een melding verzonden door de server van de app (publisher) naar alle ingeschreven apparaten (subscribers). Dit zorgt ervoor dat gebruikers direct op de hoogte zijn van belangrijke updates.
Hoe zitten events in elkaar?
Events is een complex onderwerp waarbij in C# meerdere onderdelen met elkaar samenwerken.
In de eerste plaats is er de publisher. Dit is een variabel die kan verwijzen naar functies (functiepointer), bijvoorbeeld in de klasse Deurbel het attribuut observer in het codevoorbeeld hieronder.
Het type van een publisher is vastgelegd. Dit type definieert de signatuur van de functies die kunnen inschrijven (subscribe). Het type wordt vastgelegd in een delegate.
In het codevoorbeeld is de signatuur van Observer en sendMail gelijk. De sendMail kan ingeschreven (subscribe) bij een publisher van het type Observer.
Een functie die voldoet aan de signatuur kan gekoppeld worden.
Codevoorbeeld
class Deurbel { public Observer? publisher; public void Ring(){ this.publisher?.Invoke(this); // this is geen type Message, wordt in de paragraaf EventArgs opgelost. } public void SubscribeForRingMessage(Obeserver: observeableMethod){ this.publisher += observeableMethod; }}public delegate void Observer(Message ringMessage);class Mailer { sendMail(Message mail) {...}}...deurbel.SubscribeForRingMessage(mailer.sendMail) // dus niet: mailer.sendMail()deurbel.Ring(); // roept mailer.sendMail aan.
Bij het aanroepen (invoke) van een event wordt doorgaans een ? gebruikt. Dit zorgt ervoor dat het invoken alleen plaatsvindt als this.publisher daadwerkelijk subscribers heeft.
Als de ? ontbreekt en this.publisher geen subscribers heeft (oftewel null of undefined is), kan dit resulteren in een foutmelding, omdat geprobeerd wordt een niet-bestaande methode aan te roepen. Het toevoegen van de ? voorkomt deze fout door eerst te controleren of er subscribers zijn voordat het event wordt geactiveerd.
Een belangrijk probleem in het codevoorbeeld is het toestaan van het overschrijven van alle ingeschreven functies (subscribe) buiten de publisher. Hieronder is dit probleem weergegeven.
De oplossing voor dit probleem is het uitbreiden van de signatuur van de delegate met het keyword event.
Codevoorbeeld
class Deurbel { public event Observer? publisher;}deurbel.publisher = mailer.sendMail; //FOUT en ERRORdeurbel.publisher += mailer.sendMail; //GOED
De poging in het codevoorbeeld om alle ingeschreven functies te overschrijven zal door de compiler niet geaccepteerd worden.
Eventargs
Als een event optreedt, en de publisher de ingeschreven functies gaat aanroepen, dan moet de juiste informatie worden meegegeven. Deze informatie zijn de EventArgs.
De standaard signatuur van een delegate is void (object sender, EventArgs e). De aanroep wordt dan: observer?.Invoke(this, new EventArgs()).
Door van EventArgs te erven is het mogelijk om specifieke informatie aan het event mee te geven, zoals hieronder is weergegeven.
Codevoorbeeld
class MessageEventArgs : EventArgs { public string Ontvanger {get; set} public string Tekst {get; set;}}public delegate void Observer(object sender, MessageEventArgs message);class Mailer { sendMail(object sender, MessageEventArgs e) { // ontvangen gegevens: // Ontvanger : software@deurbel.nl // Tekst : Er is aangebeld! }}//publish in Deurbelobserver?.Invoke(this, new MessageEventArgs{ Ontvanger = 'software@deurbel.nl', Message = 'Er is aangebeld!'})
EventHandler
Het declareren van het type via een delegate kan verkort worden door middel van de built-in delegate EventHandler. Dus:
public event EventHandler? Klik; is gelijk aan public delegate event Klik(object sender, EventArgs e). Door gebruik te maken van de generic type parameter is het mogelijk om custom eventargs te gebruiken: EventHandler.
Hoe gebruik je events?
Je gebruikt events in C# wanneer je een losgekoppelde manier nodig hebt om objecten te laten reageren op bepaalde gebeurtenissen. Dit is handig in situaties waarin een publisher niet hoeft te weten welke subscribers er luisteren.
Veelvoorkomende situaties waarin je events gebruikt:
Gebruikersinterfaces (UI) en interacties
Wanneer een knop wordt ingedrukt of een invoerveld verandert, worden events gebruikt om de juiste actie uit te voeren.
Asynchrone operaties en achtergrondtaken
Events kunnen aangeven wanneer een taak, zoals een bestand downloaden of een database query, is voltooid.
Observer-patroon en publish-subscribe systemen
Bijvoorbeeld bij een logging-systeem waarin meerdere componenten geïnteresseerd kunnen zijn in foutmeldingen.
Events implementeer je in de volgende stappen:
Bepaal de publisher
Geef de publisher een type
Leg het type vast in een delegate en gebruik keyword event
Maak een klasse die erft van EventArgs en bepaal eigenschappen
Geef de klasse van de publisher een methode die een event start
Bepaal de subscribers en koppel deze aan de publisher
In onderstaande casus is het voorbeeld uitgewerkt.
Casus
// Definieert een class die extra informatie bevat voor het event.class MessageEventArgs : EventArgs { // Eigenschap voor de ontvanger van het bericht. public string Ontvanger { get; set; } // Eigenschap voor de tekst van het bericht. public string Tekst { get; set; } }// Declareert een delegate-type voor event handlers.public delegate void Observer(object sender, MessageEventArgs message);// Klasse die een observer implementeert.class Mailer { // Methode die aangeroepen wordt wanneer een bericht wordt ontvangen. public void SendMail(object sender, MessageEventArgs e) { // Simuleert het versturen van een e-mail. Console.WriteLine($"E-mail naar: {e.Ontvanger}, Bericht: {e.Tekst} ); }}// Klasse die fungeert als event publisher.class Deurbel { // Event op basis van de Observer-delegate. public event Observer? observer; // Methode om een event te triggeren. public void BelDrukken() { // Controleert of er een subscriber is voordat het event wordt getriggerd. observer?.Invoke(this, new MessageEventArgs { Ontvanger = "software@deurbel.nl", Tekst = "Er is aangebeld!" }); }}// Hoofdprogramma om de deurbel en mailer te koppelen.class Program { static void Main() { // Instantie van Deurbel maken. Deurbel deurbel = new Deurbel(); // Instantie van Mailer maken. Mailer mailer = new Mailer(); // Mailer abonneren op het event van de deurbel. deurbel.observer += mailer.SendMail; // Simuleren van een deurbel-activiteit. deurbel.BelDrukken(); }}