Skip to content

8. Gebruikersgebeurtenissen

In het vorige hoofdstuk hebben we het begrip gebeurtenissen in verband met formuliercomponenten besproken. We gaan nu bekijken hoe we gebeurtenissen in onze eigen klassen kunnen aanmaken.

8.1. Vooraf gedefinieerde delegate-objecten

Het begrip delegate is in het vorige hoofdstuk al aan bod gekomen, maar toen werd het slechts kort behandeld. Toen we keken hoe de gebeurtenishandlers van formuliercomponenten werden gedeclareerd, kwamen we code tegen die vergelijkbaar was met het volgende:


            this.buttonAfficher.Click += new System.EventHandler(this.buttonAfficher_Click);

waarbij buttonAfficher een component was van het type [Button]. Deze klasse heeft een veld Click dat als volgt is gedefinieerd:

  • [1]: de klasse [Button]
  • [2]: de bijbehorende gebeurtenissen
  • [3,4]: de gebeurtenis Click
  • [5]: de declaratie van de gebeurtenis [Control.Click] [4].
    • EventHandler is een prototype (een model) van een methode die een delegate wordt genoemd.
    • event is een sleutelwoord dat de functionaliteit van delegate en EventHandler beperkt: een object delegate heeft meer functionaliteiten dan een object event.

De delegate EventHandler is als volgt gedefinieerd:

 

Het object delegate EventHandler verwijst naar een methodemodel:

  • met als eerste parameter een Object-type
  • met als tweede parameter het type EventArgs
  • die geen resultaat oplevert

Een methode die overeenkomt met het door EventHandler gedefinieerde model zou er als volgt uit kunnen zien:


        private void buttonAfficher_Click(object sender, EventArgs e);

Om een object van het type EventHandler aan te maken, gaat men als volgt te werk:

EventHandler evtHandler=new EventHandler(méthode correspondant au prototype  défini par le type EventHandler);

Zo kan men schrijven:

EventHandler evtHandler=new EventHandler(buttonAfficher_Click);

Een variabele van het type delegate is in feite een lijst met verwijzingen naar methoden die overeenkomen met het model van delegate. Om een nieuwe methode M toe te voegen aan de bovenstaande variabele evtHandler, gebruiken we de volgende syntaxis:

evtHandler+=new EvtHandler(M);

De notatie += kan worden gebruikt, zelfs als evtHandler een lege lijst is.

De instructie:


            this.buttonAfficher.Click += new System.EventHandler(this.buttonAfficher_Click);

voegt een methode van het type EventHandler toe aan de lijst met methoden van de gebeurtenis buttonAfficher.Click. Wanneer de gebeurtenis Click op de component buttonAfficher plaatsvindt, voert VB de instructie uit:

            buttonAfficher.Click(source, evt);

waarbij:

  • source de component van het type object is die de gebeurtenis
  • evt van het type EventArgs en bevat geen informatie

Alle ondertekeningsmethoden void M(object,EventArgs) die aan de gebeurtenis Click zijn gekoppeld via:


            this.buttonAfficher.Click += new System.EventHandler(M);

worden aangeroepen met de parameters (source, evt) die door VB zijn doorgegeven.

8.2. Delegate-objecten definiëren

De instructie


    public delegate int Opération(int n1, int n2);

definieert een type met de naam Opération dat een functieprototype vertegenwoordigt dat twee gehele getallen accepteert en een geheel getal retourneert. Het is het sleutelwoord delegate dat van Opération een definitie van een functieprototype maakt.

Een variabele op van het type Opération zal dienen om een lijst met functies op te slaan die overeenkomen met het prototype Opération:

int f1(int,int)
int f2(int,int)
...
int fn(int,int)

Het opslaan van een methode fi in de variabele op gebeurt met op=new Operatie(fi) of, eenvoudiger, met op=fi. Om een methode fj toe te voegen aan de lijst met reeds opgeslagen functies, schrijft men op+= fj. Om een reeds opgeslagen methode fk te verwijderen, schrijft men op-=fk. Als we in ons voorbeeld n=op(n1,n2) typen, worden alle methoden die zijn opgeslagen in de variabele op uitgevoerd met de parameters n1 en n2. Het opgehaalde resultaat n is dat van de laatst uitgevoerde methode. Het is niet mogelijk om de resultaten van alle methoden te verkrijgen. Om deze reden leveren methoden, wanneer ze in een gedelegeerde functie worden opgeslagen, meestal een resultaat van het type void op.

Laten we het volgende voorbeeld eens bekijken:


using System;
namespace Chap6 {
    class Class1 {
        // definitie van een functieprototype
        // neemt 2 gehele getallen als parameter en retourneert een geheel getal
        public delegate int Opération(int n1, int n2);

        // twee instantiemethoden die overeenkomen met het prototype
        public int Ajouter(int n1, int n2) {
            Console.WriteLine("Ajouter(" + n1 + "," + n2 + ")");
            return n1 + n2;
        }//toevoegen

        public int Soustraire(int n1, int n2) {
            Console.WriteLine("Soustraire(" + n1 + "," + n2 + ")");
            return n1 - n2;
        }//aftrekken

        // een statische methode die overeenkomt met het prototype
        public static int Augmenter(int n1, int n2) {
            Console.WriteLine("Augmenter(" + n1 + "," + n2 + ")");
            return n1 + 2 * n2;
        }//verhogen

        static void Main(string[] args) {

            // we definiëren een object van het type ‘operatie’ om daar functies in op te slaan
            // we registreren de statische functie verhogen
            Opération op = Augmenter;
            // de delegate wordt uitgevoerd
            int n = op(4, 7);
            Console.WriteLine("n=" + n);

            // aanmaken van een object c1 van het type class1
            Class1 c1 = new Class1();
            // de methode ‘toevoegen’ van c1 wordt in de delegate geregistreerd
            op = c1.Ajouter;
            // het delegatieobject wordt uitgevoerd
            n = op(2, 3);
            Console.WriteLine("n=" + n);
            // de methode 'aftrekken' van c1 wordt in de delegatie opgeslagen
            op = c1.Soustraire;
            n = op(2, 3);
            Console.WriteLine("n=" + n);
            //twee functies worden in de delegate opgeslagen
            op = c1.Ajouter;
            op += c1.Soustraire;
            // uitvoering van het gedelegeerde object
            op(0, 0);
            // een functie uit het delegatieobject verwijderen
            op -= c1.Soustraire;
            // de delegatie wordt uitgevoerd
            op(1, 1);
        }
    }
}
  • regel 3: definieert een klasse Class1.
  • regel 6: definitie van delegate Opération: een prototype van methoden die twee parameters van het type int accepteren en een resultaat van het type int retourneren
  • regels 9-12: de instantiemethode Ajouter heeft de handtekening van de delegate Opération.
  • regels 14-17: de instantiemethode Soustraire heeft de handtekening van de delegate Opération.
  • regels 20-23: de klassemethode Augmenter heeft de handtekening van de delegate Opération.
  • regel 25: de methode Main wordt uitgevoerd
  • regel 20: de variabele op is van het type delegate Opération. Deze zal een lijst bevatten van methoden met de signatuur van het type delegate Opération. Er wordt een eerste methodereferentie aan toegewezen, namelijk die naar de statische methode Class1.Augmenter.
  • regel 31: delegate op wordt uitgevoerd: alle methoden waarnaar door op wordt verwezen, zullen worden uitgevoerd. Dit gebeurt met de parameters die zijn doorgegeven aan delegate en op.. Hier wordt alleen de statische methode Class1.Augmenter uitgevoerd.
  • regel 35: er wordt een instantie c1 van de klasse Class1 aangemaakt.
  • regel 37: de instantiemethode c1.Ajouter wordt toegewezen aan de delegate op. Augmenter was een statische methode, Ajouter is een instantiemethode. We wilden laten zien dat dit niet van belang was.
  • regel 39: de methode delegate op wordt uitgevoerd: de methode Ajouter wordt uitgevoerd met de parameters die zijn doorgegeven aan delegate op.
  • regel 42: we doen hetzelfde met de instantiemethode Soustraire.
  • regels 46-47: de methoden Ajouter en Soustraire worden in de methoden delegate en op geplaatst.
  • regel 49: de delegate op wordt uitgevoerd: de twee methoden Ajouter en Soustraire worden uitgevoerd met de parameters die zijn doorgegeven aan delegate en op.
  • regel 51: de methode Soustraire wordt verwijderd uit delegate op.
  • regel 53: de delegate op wordt uitgevoerd: de resterende methode Ajouter wordt nu uitgevoerd.

De resultaten van de uitvoering zijn als volgt:

1
2
3
4
5
6
7
8
9
Augmenter(4,7)
n=18
Ajouter(2,3)
n=5
Soustraire(2,3)
n=-1
Ajouter(0,0)
Soustraire(0,0)
Ajouter(1,1)

8.3. Delegates of interfaces?

De begrippen delegates en interfaces lijken misschien vrij dicht bij elkaar te liggen en men kan zich afvragen wat precies de verschillen tussen deze twee begrippen zijn. Laten we het volgende voorbeeld nemen, dat dicht bij een eerder bestudeerd voorbeeld ligt:


using System;
namespace Chap6 {
    class Program1 {
        // definitie van een functieprototype
        // neemt 2 gehele getallen als parameter en retourneert een geheel getal
        public delegate int Opération(int n1, int n2);

        // twee instantiemethoden die overeenkomen met het prototype
        public static int Ajouter(int n1, int n2) {
            Console.WriteLine("Ajouter(" + n1 + "," + n2 + ")");
            return n1 + n2;
        }//toevoegen

        public static int Soustraire(int n1, int n2) {
            Console.WriteLine("Soustraire(" + n1 + "," + n2 + ")");
            return n1 - n2;
        }//aftrekken

        // Uitvoering van een delegate
        public static int Execute(Opération op, int n1, int n2){
            return op(n1, n2);
        }

        static void Main(string[] args) {
            // uitvoering van de delegatie ‘Toevoegen’
            Console.WriteLine(Execute(Ajouter, 2, 3));
            // uitvoering van de delegatie Aftrekken
            Console.WriteLine(Execute(Soustraire, 2, 3));
            // uitvoering van een multicast-delegate
            Opération op = Ajouter;
            op += Soustraire;
            Console.WriteLine(Execute(op, 2, 3));
            // Een functie uit de delegatie verwijderen
            op -= Soustraire;
            // de delegatie wordt uitgevoerd
            Console.WriteLine(Execute(op, 2, 3));
        }
    }
}

Op regel 20 verwacht de methode Execute een verwijzing naar een object van het type delegate Opération, gedefinieerd op regel 6. Hierdoor kunnen verschillende methoden (regels 26, 28, 32 en 36) worden doorgegeven aan de methode Execute. Deze eigenschap van polymorfisme kan ook worden bereikt met een interface:


using System;

namespace Chap6 {

    // interface IOperation
    public interface IOperation {
        int operation(int n1, int n2);
    }

    // klasse Toevoegen
    public class Ajouter : IOperation {
        public int operation(int n1, int n2) {
            Console.WriteLine("Ajouter(" + n1 + "," + n2 + ")");
            return n1 + n2;
        }
    }

    // klasse Aftrekken
    public class Soustraire : IOperation {
        public int operation(int n1, int n2) {
            Console.WriteLine("Soustraire(" + n1 + "," + n2 + ")");
            return n1 - n2;
        }
    }

    // testklasse
    public static class Program2 {
        // Uitvoering van de enige methode van de interface IOperation
        public static int Execute(IOperation op, int n1, int n2) {
            return op.operation(n1, n2);
        }

        public static void Main() {
            // uitvoering van de delegatie 'Toevoegen'
            Console.WriteLine(Execute(new Ajouter(), 2, 3));
            // uitvoering van de delegatie 'Aftrekken'
            Console.WriteLine(Execute(new Soustraire(), 2, 3));
        }
    }
}
  • regels 6-8: de interface [IOperation] definieert een methode operation.
  • regels 11-16 en 19-24: de klassen [Ajouter] en [Soustraire] implementeren de interface [IOperation].
  • regels 29-31: de methode Execute, waarvan de eerste parameter van het type van de interface IOperation is. De methode Execute ontvangt achtereenvolgens als eerste parameter een instantie van de klasse Ajouter en vervolgens een instantie van de klasse Soustraire.

Hier zien we duidelijk het polymorfe karakter terug dat de parameter van het type delegate in het vorige voorbeeld had. De twee voorbeelden laten tegelijkertijd de verschillen tussen deze twee begrippen zien.

De typen delegate en interface zijn onderling uitwisselbaar

  • als de interface slechts één methode heeft. Het type delegate is namelijk een omhulling voor één enkele methode, terwijl de interface zelf meerdere methoden kan definiëren.
  • als het multicast-aspect van delegate niet wordt gebruikt. Dit multicast-concept bestaat namelijk niet in de interface.

Als aan deze twee voorwaarden is voldaan, kan er worden gekozen tussen de volgende twee handtekeningen voor de methode Execute:


int Execute(IOperation op, int n1, int n2)
int Execute(Opération op, int n1, int n2)

De tweede, die gebruikmaakt van de delegate, kan flexibeler in het gebruik zijn. In de eerste signatuur moet de eerste parameter van de methode namelijk de interface IOperation implementeren. Dit betekent dat er een klasse moet worden aangemaakt om de methode te definiëren die als eerste parameter aan de methode Execute moet worden doorgegeven. Bij de tweede signatuur volstaat elke bestaande methode met de juiste signatuur. Er hoeft geen extra constructie te worden gemaakt.

8.4. Gebeurtenisbeheer

delegate-objecten kunnen worden gebruikt om gebeurtenissen te definiëren. Een klasse C1 kan een gebeurtenis evt als volgt definiëren:

  • een type delegate wordt gedefinieerd binnen of buiten de klasse C1:
delegate TResult Evt(T1 param1, T2 param2, ...);
  • de klasse C1 definieert een veld van het type delegate Evt:
public Evt Evt1;
  • wanneer een instantie c1 van de klasse C1 een gebeurtenis wil melden, voert deze zijn delegate Evt1 uit door de parameters door te geven die zijn gedefinieerd door de delegate Evt. Alle methoden die zijn geregistreerd in de delegate Evt1 worden dan met deze parameters uitgevoerd. Men kan zeggen dat ze op de hoogte zijn gebracht van de gebeurtenis Evt1.
  • Als een c2-object dat gebruikmaakt van een c1-object op de hoogte wil worden gesteld van het optreden van de gebeurtenis Evt1 op het c1-object, dan registreert het een van zijn methoden, c2.M, in het gedelegeerde object c1.Evt1 van het object c1.. Zo wordt zijn methode c2.M telkens uitgevoerd wanneer degebeurtenis Evt1 zich voordoet op het object c1. Hij kan zich ook uitschrijven wanneer hij niet langer op de hoogte wil worden gebracht van de gebeurtenis.
  • Aangezien het gedelegeerde object c1.Evt1 meerdere methoden kan registreren, zullen verschillende objecten ci zich bij de delegatie c1.Evt1 kunnen registreren om op de hoogte te worden gesteld van de gebeurtenis Evt1 op c1.

In dit scenario hebben we:

  • een klasse die een gebeurtenis signaleert
  • klassen die over deze gebeurtenis worden geïnformeerd. Men zegt dat ze zich op de gebeurtenis abonneren.
  • een type delegate dat de signatuur definieert van de methoden die over de gebeurtenis worden geïnformeerd

Het .NET-framework definieert:

  • een standaardhandtekening van de delegate voor een gebeurtenis
public delegate void MyEventHandler(object source, EventArgs evtInfo);
  • source: het object dat de gebeurtenis heeft gemeld
  • evtInfo: een object van het type EventArgs of een daarvan afgeleid object dat informatie over de gebeurtenis verstrekt
  • de naam van het delegate-object moet eindigen op EventHandler
  • een standaardmanier om een gebeurtenis van het type MyEventHandler in een klasse te declareren:
1
2
3
4
public Class C1{
    public event MyEventHandler Evt1;
...
}

Het veld Evt1 is van het type delegate. Het sleutelwoord event is bedoeld om de bewerkingen die erop kunnen worden uitgevoerd te beperken:

  • van buiten de klasse C1 zijn alleen de bewerkingen += en -= mogelijk. Dit voorkomt dat (bijvoorbeeld door een fout van de ontwikkelaar) de methoden die op de gebeurtenis zijn geabonneerd, worden verwijderd. Men kan zich eenvoudig abonneren (+=) of afmelden (-=) voor de gebeurtenis.
  • Alleen een instantie van het type C1 kan de aanroep Evt1(source,evtInfo) uitvoeren, die de uitvoering van de methoden die op de gebeurtenis Evt1. zijn geabonneerd, in gang zet

Het .NET-framework biedt een generieke methode die voldoet aan de handtekening van de delegate van een gebeurtenis:

public delegate void EventHandler<TEventArgs>(object source, TEventArgs evtInfo) where TEventArgs : EventArgs
  • de delegate EventHandler maakt gebruik van het generieke type TEventArgs, dat het type is van zijn tweede parameter
  • het type TEventArgs moet zijn afgeleid van het type EventsArgs (waarbij TEventArgs: EventArgs)

Met dit generieke type delegate volgt de declaratie van een gebeurtenis X in klasse C het volgende aanbevolen schema:

  • definieer een type XEventArgs, afgeleid van EventArgs, om de informatie over de gebeurtenis X in te kapselen
  • definieer in klasse C een gebeurtenis van het type EventHandler<XEventArgs>.
  • definieer in klasse C een beschermde methode
protected void OnXHandler(XEventArgs e);

die bedoeld is om de gebeurtenis X aan abonnees te "publiceren".

Laten we het volgende voorbeeld bekijken:

  • een klasse Emetteur kapselt een temperatuur in. Deze temperatuur wordt gemeten. Wanneer deze temperatuur een bepaalde drempel overschrijdt, moet er een gebeurtenis worden geactiveerd. We noemen deze gebeurtenis TemperatureTropHaute. De informatie over deze gebeurtenis wordt ingekapseld in een type TemperatureTropHauteEventArgs.
  • Een klasse Souscripteur abonneert zich op de voorgaande gebeurtenis. Wanneer deze klasse op de hoogte wordt gesteld van de gebeurtenis, geeft ze een bericht weer op de console.
  • Een consoleprogramma maakt een zender en twee abonnees aan. Het voert de temperaturen via het toetsenbord in en slaat deze op in een instantie van het type Emetteur. Als de temperatuur te hoog is, publiceert de instantie Emetteur de gebeurtenis TemperatureTropHaute.

Om te voldoen aan de aanbevolen methode voor gebeurtenisbeheer, definiëren we eerst het type TemperatureTropHauteEventArgs om de informatie over de gebeurtenis in te kapselen:


using System;

namespace Chap6 {
    public class TemperatureTropHauteEventArgs:EventArgs {
        // temperatuur tijdens de gebeurtenis
        public decimal Temperature { get; set; }
        // fabrikanten
        public TemperatureTropHauteEventArgs() {
        }
        public TemperatureTropHauteEventArgs(decimal temperature) {
            Temperature = temperature;
        }
    }
}
  • regel 6: de informatie die door de klasse TemperatureTropHauteEventArgs wordt ingekapseld, is de temperatuur die de gebeurtenis TemperatureTropHaute heeft veroorzaakt.

De klasse Emetteur is als volgt:


using System;

namespace Chap6 {
    public class Emetteur {
        static decimal SEUIL = 19;

        // gemeten temperatuur
        private decimal temperature;
        // naam van de bron
        public string Nom { get; set; }
        // event gemeld
        public event EventHandler<TemperatureTropHauteEventArgs> TemperatureTropHaute;

        // temperatuur uitlezen/schrijven
        public decimal Temperature {
            get {
                return temperature;
            }
            set {
                temperature = value;
                if (temperature > SEUIL) {
                    // de gebeurtenis wordt aan de abonnees gemeld
                    OnTemperatureTropHaute(new TemperatureTropHauteEventArgs(temperature));
                }
            }
        }

        // melding van een gebeurtenis
        protected virtual void OnTemperatureTropHaute(TemperatureTropHauteEventArgs evt) {
            // verzending van de gebeurtenis TemperatureTropHaute naar de abonnees
            TemperatureTropHaute(this, evt);
        }
    }
}
  • regel 5: de temperatuurdrempel waarboven de gebeurtenis TemperatureTropHaute wordt gepubliceerd.
  • regel 10: de zender heeft een naam om te worden geïdentificeerd
  • regel 12: de gebeurtenis TemperatureTropHaute.
  • regels 15-26: de methode get die de temperatuur weergeeft en de methode set die deze registreert. Het is de methode set die ervoor zorgt dat de gebeurtenis TemperatureTropHaute wordt gepubliceerd als de te registreren temperatuur de drempelwaarde van regel 5 overschrijdt. Deze methode zorgt ervoor dat de gebeurtenis wordt gepubliceerd via de methode OnTemperatureTropHauteHandler uit regel 29, door als parameter een object TemperatureTropHauteEventArgs door te geven waarin de temperatuur is opgeslagen die de drempelwaarde heeft overschreden.
  • regels 29-32: de gebeurtenis TemperatureTropHaute wordt gepubliceerd met als eerste parameter de zender zelf en als tweede parameter het object TemperatureTropHauteEventArgs dat als parameter is ontvangen.

De klasse Souscripteur die zich op de gebeurtenis TemperatureTropHaute gaat abonneren, is als volgt:


using System;

namespace Chap6 {
    public class Souscripteur {
        // naam
        public string Nom { get; set; }

        // beheerder van de gebeurtenis TemperatureTropHaute
        public void EvtTemperatureTropHaute(object source, TemperatureTropHauteEventArgs e) {
            // weergave op de bedieningsconsole
            Console.WriteLine("Souscripteur [{0}] : la source [{1}] a signalé une température trop haute : [{2}]", Nom, ((Emetteur)source).Nom, e.Temperature);
        }
    }
}
  • regel 6: elke abonnee wordt geïdentificeerd door een naam.
  • regels 9-12: de methode die aan de gebeurtenis TemperatureTropHaute wordt gekoppeld. Deze heeft de handtekening van het type delegate EventHandler<TEventArgs> die een gebeurtenisbeheerder moet hebben. De methode geeft op de console het volgende weer: de naam van de abonnee die het bericht weergeeft, de naam van de verzender die de gebeurtenis heeft gemeld, de temperatuur die de gebeurtenis heeft geactiveerd.
  • De inschrijving op de gebeurtenis TemperatureTropHaute van een object Emetteur vindt niet plaats in de klasse Souscripteur. Dit gebeurt door een externe klasse.

Het programma [Program.cs] koppelt al deze elementen aan elkaar:


using System;
namespace Chap6 {
    class Program {
        static void Main(string[] args) {
            // aanmaken van een gebeurtenisverzender
            Emetteur e1 = new Emetteur() { Nom = "e" };
            // aanmaken van een tabel met 2 abonnees
            Souscripteur[] souscripteurs = new Souscripteur[2];
            for (int i = 0; i < souscripteurs.Length; i++) {
                // een abonnee aanmaken
                souscripteurs[i] = new Souscripteur() { Nom = "s" + i };
                // de abonnee wordt aangemeld voor de gebeurtenis TemperatureTropHaute van e1
                e1.TemperatureTropHaute += souscripteurs[i].EvtTemperatureTropHaute;
            }
            // de temperaturen worden via het toetsenbord ingevoerd
            decimal temperature;
            Console.Write("Température (rien pour arrêter) : ");
            string saisie = Console.ReadLine().Trim();
            // zolang de ingevoerde regel niet leeg is
            while (saisie != "") {
                // is de invoer een decimaal getal?
                if (decimal.TryParse(saisie, out temperature)) {
                    // juiste temperatuur – deze wordt opgeslagen
                    e1.Temperature = temperature;
                } else {
                    // er wordt een foutmelding gegeven
                    Console.WriteLine("Température incorrecte");
                }
                // nieuwe invoer
                Console.Write("Température (rien pour arrêter) : ");
                saisie = Console.ReadLine().Trim();
            }//while
        }
    }
}
  • regel 6: aanmaken van de zender
  • regels 8-14: aanmaken van twee abonnees die worden geabonneerd op de gebeurtenis TemperatureTropHaute van de zender.
  • regels 20-32: lus voor het invoeren van temperaturen via het toetsenbord
  • regel 24: als de ingevoerde temperatuur correct is, wordt deze doorgegeven aan het object Emetteur e1, dat de gebeurtenis TemperatureTropHaute activeert als de temperatuur hoger is dan 19 °C.

De resultaten van de uitvoering zijn als volgt:

1
2
3
4
Température (rien pour arrêter) : 17
Température (rien pour arrêter) : 21
Souscripteur [s0] : la source [e] a signalé une température trop haute : [21]
Souscripteur [s1] : la source [e] a signalé une température trop haute : [21]