Gå til indholdet

Hændelser

Du har nu kendskab nok til at kunne forstå og tilføje den sidste medlemstype til klasser og strukturer – nemlig hændelser. Efter dette kapitel er pladen fuld, og du har forståelse for samtlige medlemstyper i klasser:

  • felter
  • egenskaber
  • metoder
  • hændelser

En hændelse i en klasse giver mulighed for, at objekter af klassen kan afvikle en eller flere metoder, når en eller anden hændelse sker. Du kan se det lidt som at give objekterne liv. Og med tanke på forrige kapitel sker afvikling af en eller flere metoder naturligvis ved hjælp af delegates.

Information til undervisere

Traditionelle hændelser i C# er en af de ting som bruges noget mindre nu en tidligere. I stedet bruges ofte en mere moderne tilgang med observatører og udgivere (se eksempelvis Coravel eller RabbitMQ). Det er dog stadig vigtigt at forstå hændelser, da de er en del af C# og .NET.

Hvis du kender til brugerflade-applikationer, kender du allerede til hændelser, for mange af den type applikationer er bygget op omkring hændelsesorienteret kode. I en Windows- eller mobil-applikation er funktionalitet typisk bundet op på hændelser som eksempelvis et klik på en knap, valg på en liste, minimering af et vindue og så videre.

Brug af delegates

Den samme funktionalitet kan du indbygge i dine klasser. I vores terning kunne du eksempelvis vælge at tilføje en Rystet-hændelse. Den afvikler eventuel tilknyttet kode, hver gang terningen bliver rystet. Du kan vælge at kode det ved brug af almindelig delegates, som du lærte i forrige kapitel – eksempelvis:

class Terning
{
    public int Værdi { get; private set; }
    public Action Rystet; 

    public void Ryst() {
        this.Værdi = new Random().Next(1, 7);
        if (Rystet != null)
            Rystet.Invoke();
    }

    public Terning()
    {
        this.Værdi = 1;
    }
}

Bemærk, at terningen har et offentligt felt af typen Action (void uden argumenter), og i Ryst-metoden kontrolleres der, om den er forskellig fra null, og hvis den er, så bliver de metoder, som er gemt i delegate objektet, afviklet.

Nu kan terningen benyttes som følger:

Terning t = new Terning();
t.Rystet = () => Console.WriteLine("Terning er rystet!");

t.Ryst();                       // Terning er rystet!
Console.WriteLine(t.Værdi);     // [Tilfældig værdi]

Når terningen bliver rystet, så bliver metoden (her et lambda-udtryk) gemt i delegate objektet afviklet, men pointen er, at der kan afvikles præcis den kode, der ønskes. Det er ikke længere udvikleren af terningen, der tager stilling til, hvad der skal ske, men brugeren af terningen som styrer en eventuel funktionalitet. Hvis du som udvikler af terningen ønsker at give lidt mere information, kan du blot ændre delegate definitionen. Hvad med at fortælle, hvad terningen blev rystet til og tidspunktet:

class Terning
{
    public int Værdi { get; private set; }
    public Action<int, DateTime> Rystet; 

    public void Ryst()
    {
        this.Værdi = new Random().Next(1, 7);
        if (Rystet != null)
            Rystet.Invoke(this.Værdi, DateTime.Now);
    }

    public Terning()
    {
        this.Værdi = 1;
    }
}

Nu er der lidt flere argumenter på hændelsen, så den skal benyttes som:

Terning t = new Terning();
t.Rystet = (v, t) => Console.WriteLine(
        $"Terning er rystet kl {t.ToLongTimeString()} til en {v}'er");

t.Ryst();   // Terning er rystet kl 18:49:17 til en 4'er
Console.WriteLine(t.Værdi);     // 4

Brug af event-kodeordet

Koden i tidligere afsnit virker fint, og du må gerne skrive dine hæn-delser på den måde. Der er dog intet, der over for brugeren af klassen indikerer, at der er tale om en hændelse (det er jo blot en delegate), og samtidig gør rene delegate objekter klassen noget følsom over for eventuelle null referencer, samt yderligere uhensigtsmæssigheder. Yderligere er offentlige felter som tidligere nævnt ikke et godt valg.

Derfor har Microsoft tilføjet et event-kodeord, som dels markerer en delegate som en decideret hændelse, og dels autogenererer kompileren noget kode til beskyttelse af feltet. Det eneste, du skal gøre, er at tilføje event-kodeordet:

class Terning
{
    public int Værdi { get; private set; }
    public event Action<int, DateTime> Rystet;

    public void Ryst()
    {
        this.Værdi = new Random().Next(1, 7);
        if (Rystet != null)
            Rystet.Invoke(this.Værdi, DateTime.Now);
    }

    public Terning()
    {
        this.Værdi = 1;
    }
}

Nu opfatter kompileren, at der er tale om en hændelse, og som det fremgår af klassediagrammet, så ved Visual Studio også, at feltet ikke er et almindeligt felt (bemærk lynet). Set fra brugerne af klassen er der også en enkelt ændring. For at kunne tilføje referencer til metoder, skal operatoren += benyttes:

using System;

namespace Demo
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            Terning t = new Terning();
            t.Rystet += (v, t) => 
                Console.WriteLine($"Terning er rystet " +
                $"kl {t.ToLongTimeString()} til en {v}'er");

            t.Ryst(); // Terning er rystet kl 18:55:17 til en 4'er
            Console.WriteLine(t.Værdi);// 4
        }
    }

    class Terning
    {
        public int Værdi { get; private set; }
        public event Action<int, DateTime> Rystet;

        public void Ryst()
        {
            this.Værdi = new Random().Next(1, 7);
            if (Rystet != null)
                Rystet.Invoke(this.Værdi, DateTime.Now);
        }

        public Terning()
        {
            this.Værdi = 1;
        }
    }
}

Du bestemmer selv, hvor mange hændelser en klasse skal have, og hvilken signatur brugeren af klassen skal bruge. I terningen kan du måske overveje en BlevSekser-hændelse, fordi mange spil har særligt fokus på seksere. Måske kan hændelsen også i kaldet give besked om, hvor mange seksere der er slået.

Her er mit forslag, men prøv selv:

class Terning
{
    public int Værdi { get; private set; }
    public event Action<int, DateTime> Rystet;
    public event Action<int> BlevSekser;
    private int antalSeksere = 0;

    public void Ryst()
    {
        this.Værdi = new Random().Next(1, 7);
        if (Rystet != null)
            Rystet.Invoke(this.Værdi, DateTime.Now);
        if (this.Værdi == 6)
            antalSeksere++;
        if (BlevSekser != null)
            BlevSekser.Invoke(antalSeksere);
    }

    public Terning()
    {
        this.Værdi = 1;
    }
}

Prøv at tilføje koden til en applikation og se om du kan bruge terningen.

Så en hændelse er altså blot en almindelig delegate, som er dekoreret med event-kodeordet. Om du binder lambda-udtryk eller konkrete metoder til delegate-objektet er underordnet, men signaturen skal passe jævnfør kapitlet om delegates.

EventHandler

I mange af Microsofts applikationstyper benyttes en speciel signatur på en hændelse – void metode, der som argument tager et Object (typisk kaldt sender), der repræsenterer det objekt, som kalder metoden, samt et EventArgs-objekt (typisk kaldt e), der repræsenterer eventuelle yderligere informationer om hændelsen. Microsoft har derfor indbyggede delegates kaldet EventHandler og EventHandler, der repræsenterer denne signatur. Den finder du blandt andet i forskellige Windows-applikationer, og du må også gerne bruge den selv.

Her er terningen igen med en tilrettet Ryst-hændelse:

using System;

namespace MinTest
{
    class Program
    {
        static void Main(string[] args)
        {
            Terning t = new Terning();
            t.Rystet += (s, e) => Console.WriteLine("Rystet");
            t.Ryst();
        }
    }

    class Terning
    {
        public int Værdi { get; private set; }
        public event EventHandler Rystet;

        public void Ryst()
        {
            this.Værdi = new Random().Next(1, 7);
            if (Rystet != null)
                Rystet.Invoke(this, new EventArgs());           
        }

        public Terning()
        {
            this.Værdi = 1;
        }
    }
}

Hændelsen benytter den indbyggede EventHandler, som ikke giver yderligere informationer om hændelsen.

Hvis du ønsker at oplyse mere i hændelsen, må du skabe en klasse, der arver fra EventArgs, og udvide denne. Så kan du tilføje yderligere informationer. Her benyttes en klasse, som indeholder information om, hvornår terningen er rystet:

using System;

namespace MinTest
{
    class Program
    {
        static void Main(string[] args)
        {
            Terning t = new Terning();
            t.Rystet += (s, e) => 
               Console.WriteLine("Rystet " + e.TidForRyst);
            t.Ryst();
        }
    }

    class Terning
    {
        public int Værdi { get; private set; }
        public event EventHandler<MinEventArgs> Rystet;

        public void Ryst()
        {
            this.Værdi = new Random().Next(1, 7);
            if (Rystet != null)
                Rystet.Invoke(this, 
                    new MinEventArgs() { TidForRyst = DateTime.Now });           
        }

        public Terning()
        {
            this.Værdi = 1;
        }
    }

    class MinEventArgs : EventArgs {
        public DateTime TidForRyst { get; set; }
    }
}

Bemærk klassen MinEventArgs, som kan indeholde yderligere informationer. Men det er helt op til dig, om du ønsker at benytte Microsofts EventHandler eller skabe din helt egen delegate.

Opgaver