Object georiënteerd programmeren (deel 22)

Inheritance

Basis inheritance

Inheritance of overerving betekent dat we een klasse gaan afleiden uit een andere klasse. Deze nieuwe klasse krijgt dan alle members (velden, methods, properties en constructors) van zijn basisklasse. Dit principe is ook in de echte wereld van toepassing. We weten bijvoorbeeld dat elke auto een gaspedaal, een rempedaal, een motorkap, … heeft. Het is dus niet nodig om bij elk nieuw type auto deze dingen opnieuw te gaan modelleren. Ze zijn immers hetzelfde als in het algemene concept auto. Om dit toe te passen op onze klasse “Persoon” kunnen we zeggen dat elke werknemer een persoon is. We weten dat een persoon een naam heeft, een achternaam, … Al deze eigenschappen zijn eveneens van toepassing op een werknemer. Een werknemer heeft echter nog andere eigenschappen die niet van toepassing zijn op een persoon. Een werknemer heeft bijvoorbeeld een naam van een departement waarin hij werkt. We duiden deze relatie als volgt aan: Elke werknemer is een persoon, maar niet elke persoon is een werknemer.
Om dit te bekomen in code gebruiken we de volgende syntax:

public class Persoon
 {
     private string _naam;
     public string Naam
     {
         get { return _naam; }
         set { _naam = value; }
     }
 }
 public class Werknemer : Persoon
 {
     //Hiermee duiden we de overerving aan 
     private string _departement;
     public string Departement
     {
         get { return _departement; }
         set { _departement = value; }
     }
 }

We hebben nu twee klasses: persoon en werknemer. Een persoon heeft één property (naam), een werknemer heeft twee properties: Naam (overgeërfd van de klasse persoon) en Departement (enkel gedefinieerd in de klasse Werknemer. Dit laat ons toe om bestaande objecten uit te breiden met nieuwe functionaliteit zonder het originele aan te passen. Het laat ons ook toe om verschillende types objecten te behandelen op dezelfde manier. Aangezien een werknemer ook een persoon is, kunnen we de werknemer behandelen als een persoon, zonder te weten dat het specifiek een werknemer is. Dit slaat op de oo-eigenschap polymorfisme.

member overriding

Wanneer we een afgeleide klasse hebben, kunnen we deze afgeleide klasse het bestaande gedrag of staat van de basisklasse laten wijzigen. We weten bijvoorbeeld dat zowel een Ferrari als een Lada een gaspedaal heeft, aangezien het beide auto’s zijn. Maar we weten ook dat wanneer we op het gaspedaal van de Ferrari drukken de snelheid veel sneller zal stijgen (en de inhoud van de benzinetank omgekeerd evenredig) dan wanneer we het gaspedaal van de Lada indrukken. Daarom kunnen we bepaalde members die in de basisklasse gedefinieerd zijn overriden.
Wanneer we een member overriden, moeten we dezelfde structuur van de memberdeclaratie aanhouden (zelfde naam, zelfde datatypes, zelfde aantal en type parameters, …). De member, die gedefinieerd wordt in de afgeleide klasse, zal de functionaliteit van de basisklasse vervangen. Syntactisch gezien moeten we hiervoor twee aanpassingen doen

  • We moeten de member in de basisklasse definiëren als Virtual
  • In de afgeleide klasse moeten we de nieuwe member markeren met
    het keyword Override.

Om een nuttig voorbeeld te hebben gaan we in de klasse Persoon een methode toevoegen, bijvoorbeeld BererkenSalaris(). We markeren deze methode meteen als Overridable en voorzien een standaard implementatie.

public virtual int BerekenSalaris() 
{ 
 return 0; 
}

Vermits de klasse werknemer overerft van de klasse Persoon, heeft de klasse werknemer nu ook een method waarmee we zijn salaris kunnen opvragen. Een werknemer zal echter een salaris ontvangen dat hoger is dan 0 (liefst…). Daarom zullen we het standaardgedrag moeten gaan wijzigen. Dit doen we door de method te markeren met override.

public override int BerekenSalaris() 
{ 
 return brutobedrag - belastingen; 
}

We kunnen nu deze methode oproepen voor zowel elke persoon (dus ook voor een werknemer, want een werknemer is een persoon). Wanneer we echter de methode oproepen op een werknemer zal het resultaat verschillend zijn.

Sealed klasses

.NET laat ons toe om van eender welke klasse over te erven. Om veiligheidsredenen moeten we dus een mechanisme voorzien zodat we een aantal klasses kunnen beveiligen tegen het overerven door andere klasses. Een klasse die op deze manier beveiligd is, noemen we een sealed klasse.
We kunnen dit doen met behulp van het keyword Sealed. In onderstaand voorbeeld markeren we de klasse Persoon met het keyword Sealed. Dit heeft tot gevolg dat wanneer we willen overerven van deze klasse, we een compiler-error krijgen.

public sealed class Persoon
{
}

Het spreekt voor zich dat we ook compiler-errors zullen krijgen wanneer we een member van deze klasse nu zouden gaan declareren als Overridable.

Abstracte klasses

Anders dan sealed klasses, zijn de abstracte klasses. Bij een abstracte klasse geven we aan dat er een andere klasse MOET overerven van deze klasse. Dit geven we aan om aan te duiden dat er geen instanties kunnen gemaakt worden van deze klasse. We leggen even een link naar de echte wereld om het nu hiervan aan te duiden.
Stel we hebben een klasse Auto. Deze klasse definieert de basiseigenschappen en gedragingen die een typische auto bezit (kleur, gaspedaal, benzinetank, …). Wanneer we nu echter een echte auto willen bouwen, hebben we aan deze informatie niet genoeg, we moeten meer weten. We kunnen me andere woorden nooit een auto produceren zonder effectief te weten om welk type auto het gaat. Alvorens we een auto maken, moeten we weten of we een Golf, een Lada of een Ferrari aan het bouwen zijn.

We kunnen dit in code bekomen door een klasse het keyword abstract mee te geven. Dit zorgt ervoor dat we geen nieuwe instanties kunnen aanmaken van deze klasses, maar enkel van zijn afgeleide klasses.

public abstract class Persoon
{
}

We krijgen nu een compiler-error wanneer we een nieuwe instantie van de klasse Persoon zouden willen maken. Wanneer we overerven van een klasse die gedefinieerd is als abstract zijn we natuurlijk niet verplicht om de members te Overriden. Uiteindelijk is het de bedoeling van Inheritance om gemeenschappelijke functionaliteiten in één plaats te kunnen bundelen. In sommige gevallen wil je echter de afgeleide klasses kunnen forceren om zelf een implementatie te geven aan een bepaalde method. We nemen hiervoor een voorbeeld van de klasse Shape. De klasse shape heeft een aantal methods zoals BerekenOppervlakte, want we weten dat we van elke vorm zijn oppervlakte kunnen berekenen. Het heeft echter geen nut om de code hiervoor in de Shape-klasse te steken aangezien elke shape zijn oppervlakte anders berekend wordt. Daarom moeten we de afgeleide klasses verplichten om deze methode te overriden. Dit doen we met het keyword abstract.

public abstract class Shape
{
    public abstract int BerekenOppervlakte();
}

Merk op dat in bovenstaand code-voorbeeld we nergens “End Function” hebben staan. Dit komt omdat een member dat gedefinieerd is als Abstract geen body heeft, het wordt immers toch altijd overridden.
Wat deze declaratie doet, is ervoor zorgen dat alle klasses die afleiden van deze klasse tenminste deze functie implementeren, inclusief het aantal parameters en de juiste datatypes.

Interfaces

Klasses stellen zoals we als gezien hebben een aantal members open voor de buitenwereld. Dit kunnen we bekijken we als een contract tussen de klasse en de buitenwereld. Indien we objecten willen gebruiken van het type Persoon, moeten we dit doen door de gedefinieerde members (naam, BerekenSalaris, …) aan te spreken.

Interfaces laten ons toe om een contract op te leggen voor bepaalde klasses. Ook in de echte wereld vinden we dit terug. Bij alle elektronische apparatuur zullen we bijvoorbeeld een aan/uit-knop vinden. Dit is een gezamenlijke interface die alle elektronische apparaten delen. Omdat dit een algemene conventie is, zal iedereen het logisch vinden dat je een stofzuiger moet aanzetten, alvorens je hem kan gebruiken.

De aan/uit-knop op een elektronisch apparaat vormt de interface tussen jou en de
elektrische bedrading in het apparaat. De methods, properties en velden vormen de interface tussen de code die onze klasse gaat gebruiken en de klasse zelf.

Een interface legt dus een contract op aan een klasse. We kunnen dit als volgt implementeren in code:

//Dit is de interface, deze bepaalt dat elke klasse die deze 
//interface implementeert een sub moet hebben Spreek() 
public interface ISpreker
{
    void Spreek();
}
//De klasse persoon implementeer ISpreker en heeft een Sub 
Spreek() 
public class Persoon : ISpreker
{
    public void Spreek()
    {
    }
}

Hierboven bepalen we dus dat elke klasse die deze interface volgt een methode Spreek() zal moeten hebben. Wanneer we weten dat een klasse deze interface implementeert weten we dus ook meteen dat hij een methode Spreek() zal hebben, zonder dat we daarvoor de exacte details over deze klasse weten.

Dit is analoog met het voorbeeld van de elektrische apparaten. Zonder te weten over welk elektrisch apparaat het gaat, kunnen we met zekerheid zeggen dat we het kunnen inschakelen en uitschakelen.

Merk op dat een interface nergens effectieve functionaliteit bevat. Dit is identiek in de echte wereld. Hoewel we weten dat alle elektrische apparaten een aan/uit-knop hebben, kunnen we op voorhand niet zeggen hoe deze in werkelijkheid zal functioneren. Sommige apparaten schakelen gewoon de elektriciteit in, anderen zullen de elektriciteit inschakelen, een bepaald kanaal of stand kiezen en eventueel andere acties. De implementatie is afhankelijk van het object zelf, de interface definieert enkel wat de klasse moet kunnen, niet hoe het dit moet bereiken. Verder zien we ook dat we in de interface geen access identifiers gebruiken. Een interface vertelt namelijk niet dat een bepaalde eigenschap of gedrag voor iedereen beschikbaar moet zijn, of enkel voor bepaalde klasses. De interface vertelt enkel dat het beschikbaar moet zijn.

Van een interface kunnen we geen objecten maken, we moeten steeds objecten maken van een klasse die de interface implementeert. Dit is logisch aangezien de interface zelf geen functionaliteit bevat

Verschil tussen een interface en abstracte klasse

In het vorige onderdeel zagen we het onderdeel rond virtual classes. We konden ervoor zorgen dat er enkel objecten gemaakt konden worden. Tevens konden we ook de afgeleide klasses verplichten om een aantal members te implementeren.

Als we dit naast het concept van interfaces leggen, zien we dat er in wezen eigenlijk geen verschil tussen een interface en een abstracte (virtual) klasse, buiten het feit dat een abstracte zelf ook nog kan voorzien in code.

Waarom hebben we dan nog constructie zoals de interface nodig? Een eerste reden die we zouden kunnen aanhalen is het feit dat klasses slechts van één abstracte klasse kunnen overerven, maar wel meerdere interface kunnen implementeren. Dit is inderdaad een goede reden, maar belangrijker is de betekenis die we toekennen aan deze constructies.

Bij een overerving van een abstracte klasse bekomen we een “is een”-relatie. Bij de implementatie van een interface krijgen we echter een “heeft een”-relatie. We verduidelijken dit even met voorbeeld: Het merk Golf erft over van de abstracte klasse auto. De relatie die we bekomen is als volgt: Een Golf is een auto.

Als we naar onze voorbeeld met de interface gaan kijken, zien we dat elk apparaat (TV, Video, Computer, …) de interface Elektrisch apparaat implementeert. We benoemen de relatie dan ook als volgt:
Een TV heeft een aan/uit-knop. Praktisch gezien kunnen we deze twee items in vele gevallen door elkaar gebruiken. Het is echter belangrijk om te letten op de betekenis die je wilt geven aan een relatie tussen twee klasses. Indien je moet kiezen tussen een interface en een abstracte klasse, probeer je dan de relatie voor te stellen die er heerst tussen deze twee klasses.

Specifieke .Net interfaces

In .NET zijn er een aantal interfaces gedefinieerd die we kunnen implementeren in onze eigen klasses. Deze interfaces zorgen ervoor dat we het contract van de klasse kunnen aantonen aan andere mogelijkheden in het .NET-framework. Hoe dit in zijn werk gaat, zullen we meteen zien bij de eerste .NET-interface die we gaan bespreken.

IComparable

Wanneer we een klasse deze interface laten implementeren moeten we een methode in de klasse definiëren die de huidige klasse vergelijkt met een andere en aangeeft of de huidige klasse groter, kleiner of gelijk is aan de andere. Wanneer we zelf een custom-klasse maken is het immers onmogelijk voor het .NET-framework om te weten op basis van wat deze vergeleken zullen worden (vergelijken we personen op basis van leeftijd, grootte, … ?). Wanneer we de interface implementeren krijgen we volgende code:

//De klasse persoon implementeer ICommparable 
public class Persoon : IComparable<Persoon>
{
    private int _leeftijd;
    public int Leeftijd
    {
       get { return _leeftijd; }
       set { _leeftijd = value; }
    }
    //Deze methode geeft -1, 0 of 1 terug afhankelijk 
    //van het feit of het huidige object kleiner, gelijk of 
    //groter is dan het andere object (other) 
    public int CompareTo(Persoon other)
    {
       if (Leeftijd > other.Leeftijd)
       {
          return 1;
       }
       else if (Leeftijd < other.Leeftijd)
       {
          return -1;
       }
       else
       {
          return 0;
       }
    }
}

Door de klasse IComparable te implementeren laten we aan alle andere klasses (ook degene die onderdeel zijn van het .NET-framework) weten dat we objecten van het type Persoon kunnen vergelijken. Dit heeft een aantal handige gevolgen. Wanneer we lijsten gaan maken (in het onderdeel Arrays en collections) zullen we zien dat we deze lijsten nu eenvoudig weg kunnen sorteren door de methode Sort() op te roepen. Inwendig gaat het framework herkennen dat de objecten van het type Persoon vergeleken kunnen worden en het zal deze code gebruiken om een lijst van personen te sorteren.

IComparer

Wanneer we twee objecten slechts op één manier willen vergelijken is de IComparable-interface alles wat we nodig hebben. Echter, in de meeste situaties, zullen we objecten op verschillende manieren willen vergelijken en sorteren. In de IComparable-interface zagen we dat slechts 1 methode voorzien was om een object te vergelijken met een ander (CompareTo). Willen we objecten op verschillende manieren met elkaar gaan vergelijken dan zullen we helper-klasses moeten maken die toelaten om twee objecten te vergelijken. We maken deze klasses en laten hen de interface IComparer implementeren. Nota: let op de naamgeving van deze interfaces:

  • IComparable
    Dit geeft aan dat we objecten van deze klasse kunnen vergelijken met een andere. We implementeren deze interface dus in de klasse zelf
  • IComparer
    Deze naam duidt aan dat we een klasse definiëren die in staat is om twee objecten te vergelijken.

In onderstaande codevoorbeelden zijn de implementaties van de properties Naam en Leeftijd weggelaten om de code korter en overzichtelijker te kunnen maken:

public class Persoon
 {
    public class CompareByName : IComparer<Persoon>
    {
       public int Compare(Persoon x, Persoon y)
       {
          if (x.Name > y.name)
          {
             return 1;
          }
          else if (x.Name < y.name)
          {
             return -1;
          }
          else
          {
             return 0;
          }
       }
    }
    public class CompareByLeeftijd : IComparer<Persoon>
    {
       public int Compare(Persoon x, Persoon y)
       {
          if (x.Leeftijd > y.Leeftijd)
          {
             return 1;
          }
          else if (x.Leeftijd < y.Leeftijd)
          {
             return -1;
          }
          else
          {
             return 0;
          }
       }
    }
}

We definiëren in de klasse Persoon twee interne klasses, CompareByLeeftijd en CompareByName. Beide klasses hebben één methode die twee objecten als parameters ontvangen. Deze twee objecten worden vergeleken (in het ene geval door de leeftijd te vergelijken, in het andere door de naam te vergelijken) en geven een resultaat terug analoog aan de CompareTo-methode van de IComparable-interface.

We hebben deze interface dus geïmplementeerd en kunnen hierdoor gaan sorteren op naam of leeftijd. We zullen dit doen door een object als parameter te geven aan de sorteerfunctie. De sorteerfunctie zal dan de juiste klasse gebruiken om de sortering te doen. Hoe dit effectief in zijn werk gaat zien we bij het onderdeel Arrays & Collections.

ICloneable

De ICloneable interface gebruiken we om aan te duiden dat we een object van de klasse kunnen kopiëren. Zoals we later zullen zien is kopiëren niet gelijk aan het toewijzen van een object aan meerdere variabelen. Meer details hierover verder in de cursus. De interface ICloneable verplicht ons om een methode “Clone()” te implementeren die ons een exacte kopie van het huidig object teruggeeft. Een voorbeeld hiervan

public class Persoon : ICloneable
{
 private int _leeftijd;
 public int Leeftijd
 {
 get { return _leeftijd; }
 set { _leeftijd = value; }
 }
 public object Clone()
 {
 Persoon kloon = new Persoon();
 kloon.Leeftijd = this.Leeftijd;
 return kloon;
 }
}

IDisposable

Zoals we eerder bij de introductie van het .NET-framework zagen wordt alle code die we produceren uitgevoerd onder de controle van de CLR (Common Language Runtime). De CLR beschermt onze code van memory leaks, security, … In sommige gevallen zullen we echter code moeten aanroepen die niet in deze runtime draait. Dit betekent dat we resources zullen gebruiken die niet onder de controle van de CLR vallen. We moeten ervoor zorgen dat deze resources deftig opgeruimd worden na gebruik, aangezien de CRL dit niet automatisch zal regelen.

Een ander voorbeeld is een databaseconnectie, indien wij deze niet opruimen na gebruik zal de connectie gebruik open blijven staan en onnodige resources blijven gebruiken.

Wanneer we deze objecten gaan gebruiken hebben we een standaard methode nodig om ervoor te zorgen dat ze deftig opgeruimd kunnen worden. Hiervoor gaan we de IDisposable-interface gebruiken.

Delegates and Events

Delegates

Wat is een delegate?

Delegates zijn een onderdeel van het .NET-framework dat ons toelaat om methodes indirect aan te roepen. Tot nu toe hebben we steeds alle methods direct aangeroepen:

Persoon Erdem = new Persoon(); 
Erdem.Wandel();

We creëren een nieuw object en roepen een methode op dat object aan. Een delegate is een object dat een verwijzing naar een method kan opslaan. We kunnen dan de delegate vertellen dat hij die methode moet aanroepen. Net zoals bij klasses, moeten we vooraleer we een nieuw delegate-object maken, eerst definiëren hoe de objecten er zullen uitzien. Zoals vermeld, kan een delegate een verwijzing opslaan naar een method. Wanneer we een delegate gaan declareren leggen we vast naar welke soort van methods de delegate-objecten kunnen verwijzen. Hiermee leggen we volgende zaken vast over de methods waarnaar de delegate-objecten kunnen verwijzen:

  • Of het een sub of een function is.
  • Het aantal parameters
  • Het type van de parameters
  • Het returntype indien het een function is

Delegates declareren

Een delegate declareren we als volgt:

 //we creëren een delegate die een sub kan bevatten met 1 parameter van het type string 
 delegate void ShowMessage(string text);
 //We creëren een delegate die een function kan bevatten, 
zonder parameters en een Integer teruggeeft 
 delegate int GetAge();

We gebruiken het keyword delegate om aan te geven dat we een delegate gaan declareren. Daarna geven we aan of het om een sub of een function gaat (in het voorbeeld een sub), vervolgens de naam van het delegate-type, daarna de parameters en hun type en uiteindelijk eventueel het return-type. Hoewel deze declaratie eruitziet alsof we een method declareren, is dit zeker niet het geval. De analogie zien we omdat we een definitie moeten geven van hoe de methods waarnaar de delegate kan verwijzen er uit zien. Het is belangrijk om deze syntaxt effectief te gaan bekijken als de declaratie van een delegate. Probeer dit te bekijken als de declaratie van een klasse.

Wanneer we een klasse gaan definiëren, gaan we naderhand objecten creëren van deze klasse. Wanneer weeen delegate gaan definiëren, gaan we naderhand objecten creëren van deze delegate.

Vooraleer we een nieuw object van deze delegate gaan creëren, gaan we eerst een method maken die voldoet aan de eisen van deze delegate declaratie. De eerste delegate-declaratie vertelt ons dat we een sub moeten hebben, met 1 parameter van het type string. Hieronder zie je een methode die aan deze voorwaarde voldoet.

public void WriteToConsole(string Text) 
{ 
 Console.WriteLine(Text); 
}

We hebben nu een delegate-declaratie die ons vertelt hoe de methods waarnaar we gaan wijzen er moeten uitzien. Verder hebben we een method die aan deze specificaties voldoet. We kunnen nu dus een delegate-object maken en deze naar onze method laten wijzen. Dit doen we als volgt:

ShowMessage deleg = new ShowMessage(WriteToConsole);

Met het keyword AddressOf geven we de method mee waarnaar de delegate moet verwijzen. Het resultaat van bovenstaande declaratie is dat we een variabele hebben, waarin een delegate van het type ShowMessage zit, die een referentie heeft naar de method WriteToConsole.

Delegates aanroepen

Nu we een delegate-declaratie hebben gedaan, een methode geschreven die aan deze declaratie voldoet en een object hebben, kunnen we de delegate aanroepen.
Wanneer we de delegate gaan aanroepen, gaat de delegate de oproep doorsturen naar de methode waarnaar hij een referentie heeft. We kunnen dit als volgt doen:

deleg.Invoke("Mijn tekst");

We roepen de method Invoke aan op het delegate-object. Aangezien de method waarnaar we verwijzen 1 parameter verwacht, moeten we deze ook meegeven als we de delegate gaan oproepen.
Dit zorgt ervoor dat (inderect, via de delegate) de method WriteToConsole wordt opgeroepen.
Dit is natuurlijk een behoorlijke omweg om een simpele boodschap naar het consolevenster te sturen. De kracht van delegates wordt echter pas duidelijk wanneer we een functie op vele plaatsen gaan aanroepen. Een kort voorbeeld ter illustratie:

Code zonder delegates
We hebben een klasse waarin we in drie verschillende methods een
resultaat naar het scherm willen tonen:

public class Display
{
 public void methode1()
 {
    WriteToConsole("Dit is een boodschap");
 }
 public void methode2()
 {
    WriteToConsole("Dit is een 2e boodschap");
 }
 public void methode3()
 {
    WriteToConsole("Dit is een 3e boodschap");
 }
 public void WriteToConsole(string message)
 {
    Console.WriteLine(message);
 }
}

Stel dat we morgen beslissen om de boodschappen niet meer in het consolevenster te tonen maar weg te schrijven naar een bestand. We voegen hiervoor onderstande method toe, die ons toelaat om boodschappen naar een bestand weg te schrijven.

public void WriteToFile(string message) 
{ 
 //code om een bestand weg te schrijven 
}

Als we dit nu willen wijzigen moeten we in elke method de aanroep “WriteToConsole” gaan vervangen door “WriteToFile”.

Code met delegates:

public class Display
{
 public delegate void WriteMessage(string Message);
 private WriteMessage _deleg;
 public Display()
 {
    _deleg = new WriteMessage(WriteToConsole);
 }
 public void methode1()
 {
    _deleg.Invoke("Dit is een boodschap");
 }
 public void methode2()
 {
    _deleg.Invoke("Dit is een 2e boodschap");
 }
 public void methode3()
 {
    _deleg.Invoke("Dit is een 3e boodschap");
 }
 public void WriteToConsole(string message)
 {
    Console.WriteLine(message);
 }
 public void WriteToFile(string message)
 {
    //code om een bestand weg te schrijven 
 }
}

We declareren eerst een delegate. Deze declaratie vertelt ons dat de objecten van deze delegate een referentie kunnen hebben naar een method van het type sub, die één parameter opvraagt.

We creëren daarna een private field (_deleg) die ons delegate-object zal bevatten. Dit delegate-object creëren we in de constructor (Sub New). We laten de delegate wijzen naar de method WriteToConsole. In de methods 1,2 en 3 roepen we deze delegate aan. De delegate stuurt deze oproep dan door naar de method WriteToConsole.

Wanneer we nu plots zouden beslissen dat er naar een bestand geschreven moet worden, moeten we enkel in de constructor de verwijzing naar de method veranderen.
In dit voorbeeld is de impact nog steeds minimaal (1 wijziging ten opzichte van 3 wijzigingen). Wanneer we echter honderden aanroepen naar een methode hebben (wat in een echte applicatie vaak het geval is), gaat het verschil natuurlijk veel groter zijn.

Events

Wat is een Event?

Een event is net zoals een veld, een methode, een constructor of een property een lid van een klasse. Ze vormen een deel van de interface die de klasse aan de buitenwereld laat zien. Er is echter één groot verschil tussen events en de andere leden van een klasse. Wanneer we een veld invullen, een property toewijzen of een methode uitvoeren is het steeds de code die de klasse gaat gebruiken die het initiatief neemt. Bij events is het echter de klasse zelf die initiatief neemt en bepaalt wanneer een event gebeurt.

Een event kunnen we het makkelijkst omschrijven als een gebeurtenis. Een object zal een event laten optreden bij een belangrijke wijziging. Zo hebben we bijvoorbeeld in Windows Forms een event wanneer er op een knop geklikt wordt. De knop detecteert wanneer hij een klik ontvangt en zal dat laten weten aan de buitenwereld door een event te versturen. We kunnen deze events onderscheppen door ons in te schrijven op deze events.

In de echte wereld gebeurt in principe hetzelfde. Een organisator van een feest zal bijvoorbeeld e-mails sturen wanneer er iets staat te gebeuren. Om deze e-mails te ontvangen moet je je op voorhand inschrijven op de mailinglist.
Het sturen van de e-mails kunnen we opvatten als het raisen van een event. Het inschrijven op de mailinglist is hetzelfde als ons inschrijven op een event.

Events afhandelen

In tegenstelling tot bij de andere hoofdstukken, waar we eerst zagen hoe een klasse, veld, … te definiëren, gaan we in dit deel eerst bekijken hoe we ons kunnen inschrijven op een event en reageren op een event. Zoals gezegd heeft elke klasse 0 of meerdere events (net zoals elke klasse 0 of meerdere velden, properties, methods, …) heeft.

In Windows Forms en WPF heeft elke knop een click-event. Het click-event dicteert ons dat we dit moeten afhandelen met een methode (sub) die 2 argumenten ontvangt, een argument van het type object en een argument van het type EventArgs.
Om ons in te schrijven op een event maken we gebruik van het += teken.

 abc.ProcesCompleet += ProcesCompleet_Actie; 
 //Door de += teken zal de methode ProcesCompleet_Actie uitgevoerd worden op het moment 
 //dat het event ProcesCompleet plaats vindt in het object abc.

 public static void ProcesCompleet_Actie ()
 {
     Console.WriteLine("Process uitgevoerd!");
 }
//uitschrijven kan met het -= teken
abc.ProcesCompleet -= ProcesCompleet_Actie; 

Events versturen

Nu we weten hoe we kunnen reageren op events en ons kunnen in- en uitschrijven op events van andere objecten, gaan we leren hoe we zelf zulke events kunnen definiëren en versturen.
Om events te kunnen versturen moeten we 2 zaken doen:

  • Aangeven dat we events gaan versturen, en onder welke vorm.
  • Het event versturen

Om een event te definiëren gebruiken we het keyword Event. Dit doen we als volgt:

public class Persoon
{
 public event NameChangedEventHandler NameChanged;
 public delegate void NameChangedEventHandler(object
 Sender, EventArgs e);
}

We definiëren hier dat de klasse persoon een event zal raisen wanneer zijn naam verandert. We zeggen ook meteen dat eventhandlers 2 argumenten moeten aannemen (Sender en e).
Nadat we hebben aangegeven dat we events gaan versturen, moeten we ook op het juiste moment het event versturen. In onderstaande code zie je een volledig werkend voorbeeld:

public class Persoon
{
 public event NameChangedEventHandler NameChanged;
 public delegate void NameChangedEventHandler(object
 Sender, EventArgs e);
 private string _name;
 public string Name
 {
     get { return _name; }
     set
     {
         _name = value;
         if (NameChanged != null)
         {
            NameChanged(this, EventArgs.Empty);
         }
     }
 }
}

We definiëren eerst dat we een event NameChanged naar buiten brengen. Vervolgens gebruiken we het keyword RaiseEvent om het event te versturen wanneer de property Name gewijzigd wordt. Als argumenten geven we aan het event als Sender, Me(het huidige object) en EventArgs.Empty (leeg EventArgs argument).