Skip to content

8. Zdarzenia użytkownika

W poprzednim rozdziale omówiliśmy pojęcie zdarzeń związanych z komponentami formularza. Teraz przyjrzymy się, jak tworzyć zdarzenia w naszych własnych klasach.

8.1. Wstępnie zdefiniowane obiekty delegujące

Pojęcie obiektu delegate pojawiło się już w poprzednim rozdziale, ale wówczas omówiono je tylko pobieżnie. Kiedy przyglądaliśmy się, w jaki sposób deklarowane są procedury obsługi zdarzeń komponentów formularza, natrafiliśmy na kod podobny do poniższego:


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

gdzie buttonAfficher był komponentem typu [Button]. Klasa ta posiada pole Click zdefiniowane w następujący sposób:

  • [1]: klasa [Button]
  • [2]: jej zdarzenia
  • [3,4]: zdarzenie Click
  • [5]: deklaracja zdarzenia [Control.Click] [4].
    • EventHandler to prototyp (szablon) metody zwany delegatem.
    • event to słowo kluczowe, które ogranicza funkcjonalność obiektu delegate oraz EventHandler: obiekt delegate ma bogatszy zestaw funkcji niż obiekt event.

delegate EventHandler jest zdefiniowany w następujący sposób:

 

delegate EventHandler oznacza wzorzec metody:

  • którego pierwszym parametrem jest typ Object
  • którego drugim parametrem jest typ EventArgs
  • nie zwracający żadnego wyniku

Metoda odpowiadająca modelowi zdefiniowanemu przez EventHandler mogłaby wyglądać następująco:


        private void buttonAfficher_Click(object sender, EventArgs e);

Aby utworzyć obiekt typu EventHandler, należy postępować w następujący sposób:

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

W ten sposób można zapisać:

EventHandler evtHandler=new EventHandler(buttonAfficher_Click);

Zmienna typu delegate jest w rzeczywistości listą odwołań do metod odpowiadających modelowi delegate. Aby dodać nową metodę M do powyższej zmiennej evtHandler, stosuje się następującą składnię:

evtHandler+=new EvtHandler(M);

Notację += można zastosować nawet wtedy, gdy evtHandler jest listą pustą.

Instrukcja:


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

dodaje metodę typu EventHandler do listy metod zdarzenia buttonAfficher.Click. Gdy wystąpi zdarzenie Click w komponencie buttonAfficher, VB wykonuje instrukcję:

            buttonAfficher.Click(source, evt);

gdzie:

  • source to komponent typu object, który wywołał zdarzenie
  • evt typu EventArgs i nie zawiera żadnych informacji

Wszystkie metody podpisywania typu void M(object,EventArgs), które zostały powiązane ze zdarzeniem Click poprzez:


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

zostaną wywołane z parametrami (source, evt) przekazanymi przez VB.

8.2. Definiowanie obiektów delegatów

Instrukcja


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

definiuje typ o nazwie Opération, który reprezentuje prototyp funkcji przyjmującej dwie liczby całkowite i zwracającej liczbę całkowitą. To słowo kluczowe delegate sprawia, że Opération jest definicją prototypu funkcji.

Zmienna op typu Opération będzie służyć do przechowywania listy funkcji odpowiadających prototypowi Opération:

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

Zapisanie metody fi w zmiennej op odbywa się za pomocą op=new Operacja(fi) lub, prościej, poprzez op=fi. Aby dodać metodę fj do listy już zapisanych funkcji, należy wpisać op+= fj. Aby usunąć już zapisaną metodę fk, należy wpisać op-=fk. Jeśli w naszym przykładzie wpiszemy n=op(n1,n2), wszystkie metody zapisane w zmiennej op zostaną wykonane z parametrami n1 i n2. Otrzymany wynik n będzie wynikiem ostatniej wykonanej metody. Nie ma możliwości uzyskania wyników wygenerowanych przez wszystkie metody. Z tego powodu, jeśli lista metod zostanie zapisana w funkcji delegowanej, metody te najczęściej zwracają wynik typu void.

Rozważmy następujący przykład:


using System;
namespace Chap6 {
    class Class1 {
        // definicja prototypu funkcji
        // przyjmuje 2 liczby całkowite jako parametry i zwraca liczbę całkowitą
        public delegate int Opération(int n1, int n2);

        // dwie metody instancji odpowiadające prototypowi
        public int Ajouter(int n1, int n2) {
            Console.WriteLine("Ajouter(" + n1 + "," + n2 + ")");
            return n1 + n2;
        }//dodaj

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

        // jedna metoda statyczna odpowiadająca prototypowi
        public static int Augmenter(int n1, int n2) {
            Console.WriteLine("Augmenter(" + n1 + "," + n2 + ")");
            return n1 + 2 * n2;
        }//zwiększ

        static void Main(string[] args) {

            // definiujemy obiekt typu operacja w celu zapisania w nim funkcji
            // zapisujemy funkcję statyczną „zwiększ”
            Opération op = Augmenter;
            // wywołujemy delegata
            int n = op(4, 7);
            Console.WriteLine("n=" + n);

            // tworzenie obiektu c1 typu class1
            Class1 c1 = new Class1();
            // w delegacie rejestruje się metodę „dodaj” obiektu c1
            op = c1.Ajouter;
            // wykonanie obiektu delegata
            n = op(2, 3);
            Console.WriteLine("n=" + n);
            // zapisywanie metody odejmowania obiektu c1 w delegacie
            op = c1.Soustraire;
            n = op(2, 3);
            Console.WriteLine("n=" + n);
            //zapisanie dwóch funkcji w delegacie
            op = c1.Ajouter;
            op += c1.Soustraire;
            // wykonanie obiektu delegowanego
            op(0, 0);
            // usuwanie funkcji z obiektu delegowanego
            op -= c1.Soustraire;
            // wykonanie delegata
            op(1, 1);
        }
    }
}
  • wiersz 3: definiuje klasę Class1.
  • wiersz 6: definicja delegate Opération: prototyp metod przyjmujących dwa parametry typu int i zwracających wynik typu int
  • wiersze 9–12: metoda instancji Ajouter ma sygnaturę delegata Opération.
  • wiersze 14–17: metoda instancji Soustraire ma sygnaturę delegata Opération.
  • wiersze 20–23: metoda klasy Augmenter ma sygnaturę delegata Opération.
  • wiersz 25: wywołano metodę Main
  • wiersz 20: zmienna op jest typu delegate Opération. Będzie zawierała listę metod o sygnaturze typu delegate Opération. Przypisuje się do niej pierwsze odwołanie do metody – do metody statycznej Class1.Augmenter.
  • wiersz 31: wykonywane są metody delegate i op: zostaną wykonane wszystkie metody, do których odwołuje się op. Zostaną one wykonane z parametrami przekazanymi do metod delegate i op.. W tym przypadku zostanie wykonana tylko metoda statyczna Class1.Augmenter.
  • wiersz 35: tworzona jest instancja c1 klasy Class1.
  • wiersz 37: metoda instancji c1.Ajouter jest przypisana do delegata op. Augmenter była metodą statyczną, Ajouter jest metodą instancji. Chcieliśmy pokazać, że nie ma to znaczenia.
  • wiersz 39: wykonana zostaje metoda delegate op: metoda Ajouter zostanie wykonana z parametrami przekazanymi do delegate op.
  • wiersz 42: postępujemy analogicznie z metodą instancji Soustraire.
  • wiersze 46–47: umieszczamy metody Ajouter i Soustraire w metodach delegate i op.
  • wiersz 49: uruchamiane są metody delegate i op: obie metody Ajouter i Soustraire zostaną wykonane z parametrami przekazanymi do delegate i op.
  • wiersz 51: metoda Soustraire została usunięta z metod delegate i op.
  • wiersz 53: uruchomiono delegate oraz op: zostanie uruchomiona pozostała metoda Ajouter.

Wyniki wykonania są następujące:

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. Delegaty czy interfejsy?

Pojęcia delegates i interfejsów mogą wydawać się dość zbliżone i można się zastanawiać, jakie dokładnie są różnice między tymi dwoma pojęciami. Weźmy następujący przykład, zbliżony do już omówionego:


using System;
namespace Chap6 {
    class Program1 {
        // zdefiniowanie prototypu funkcji
        // przyjmuje 2 liczby całkowite jako parametry i zwraca liczbę całkowitą
        public delegate int Opération(int n1, int n2);

        // dwie metody instancji odpowiadające prototypowi
        public static int Ajouter(int n1, int n2) {
            Console.WriteLine("Ajouter(" + n1 + "," + n2 + ")");
            return n1 + n2;
        }//dodaj

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

        // Wykonanie delegata
        public static int Execute(Opération op, int n1, int n2){
            return op(n1, n2);
        }

        static void Main(string[] args) {
            // wykonanie delegata Dodaj
            Console.WriteLine(Execute(Ajouter, 2, 3));
            // wykonanie delegata Odejmowanie
            Console.WriteLine(Execute(Soustraire, 2, 3));
            // wykonanie delegata multiemisji
            Opération op = Ajouter;
            op += Soustraire;
            Console.WriteLine(Execute(op, 2, 3));
            // usuwanie funkcji z delegata
            op -= Soustraire;
            // wykonanie delegata
            Console.WriteLine(Execute(op, 2, 3));
        }
    }
}

W wierszu 20 metoda Execute oczekuje odwołania do obiektu typu delegata Opération zdefiniowanego w wierszu 6. Pozwala to na przekazywanie do metody Execute różnych metod (wiersze 26, 28, 32 i 36). Tę właściwość polimorfizmu można uzyskać również za pomocą interfejsu:


using System;

namespace Chap6 {

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

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

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

    // klasa testowa
    public static class Program2 {
        // Wywołanie jedynej metody interfejsu IOperation
        public static int Execute(IOperation op, int n1, int n2) {
            return op.operation(n1, n2);
        }

        public static void Main() {
            // wywołanie delegata Dodaj
            Console.WriteLine(Execute(new Ajouter(), 2, 3));
            // wywołanie delegata odejmowania
            Console.WriteLine(Execute(new Soustraire(), 2, 3));
        }
    }
}
  • wiersze 6–8: interfejs [IOperation] definiuje metodę operation.
  • wiersze 11–16 i 19–24: klasy [Ajouter] oraz [Soustraire] implementują interfejs [IOperation].
  • wiersze 29–31: metoda Execute, której pierwszy parametr jest typu interfejsu IOperation. Metoda Execute otrzyma kolejno jako pierwszy parametr instancję klasy Ajouter, a następnie instancję klasy Soustraire.

Widać tu wyraźnie polimorficzny charakter parametru typu delegate z poprzedniego przykładu. Oba przykłady pokazują jednocześnie różnice między tymi dwoma pojęciami.

Typy delegate i interface są zamienne

  • , jeśli interfejs posiada tylko jedną metodę. Typ delegate jest bowiem opakowaniem dla pojedynczej metody, podczas gdy interfejs może definiować wiele metod.
  • jeśli aspekt multiemisji typu delegate nie jest wykorzystywany. Pojęcie multiemisji nie występuje bowiem w interfejsie.

Jeśli oba te warunki są spełnione, wówczas dla metody Execute można wybrać jedną z dwóch następujących sygnatur:


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

Druga sygnatura, wykorzystująca metodę delegate, może okazać się bardziej elastyczna w użyciu. W pierwszej sygnaturze pierwszy parametr metody musi bowiem implementować interfejs IOperation. Wymaga to utworzenia klasy w celu zdefiniowania metody, która ma zostać przekazana jako pierwszy parametr do metody Execute. W przypadku drugiej sygnatury wystarczy dowolna istniejąca metoda o odpowiedniej sygnaturze. Nie ma potrzeby tworzenia żadnych dodatkowych konstrukcji.

8.4. Obsługa zdarzeń

Obiekty typu delegate mogą służyć do definiowania zdarzeń. Klasa C1 może zdefiniować zdarzenie evt w następujący sposób:

  • typ delegate jest definiowany w klasie C1 lub poza nią:
delegate TResult Evt(T1 param1, T2 param2, ...);
  • klasa C1 definiuje pole typu delegate Evt:
public Evt Evt1;
  • gdy instancja c1 klasy C1 będzie chciała zgłosić zdarzenie, uruchomi swój delegat Evt1, przekazując mu parametry zdefiniowane przez delegata Evt. Wszystkie metody zarejestrowane w delegate Evt1 zostaną wówczas wykonane z tymi parametrami. Można powiedzieć, że zostały one powiadomione o zdarzeniu Evt1.
  • jeśli obiekt c2, korzystający z obiektu c1, chce zostać powiadomiony o wystąpieniu zdarzenia Evt1 na obiekcie c1, zarejestruje jedną ze swoich metod c2.M w obiekcie delegowanym c1.Evt1 obiektu c1.. W ten sposób jego metoda c2.M będzie wykonywana za każdym razem, gdyzdarzenie Evt1 wystąpi na obiekcie c1. Będzie mógł również wyrejestrować się, gdy nie będzie już chciał otrzymywać powiadomień o tym zdarzeniu.
  • Ponieważ obiekt delegowany c1.Evt1 może zarejestrować wiele metod, różne obiekty ci będą mogły zarejestrować się u delegata c1.Evt1, aby otrzymywać powiadomienia o zdarzeniu Evt1 na obiekcie c1.

W tym scenariuszu mamy:

  • klasę, która zgłasza zdarzenie
  • klasy, które są powiadamiane o tym zdarzeniu. Mówi się, że subskrybują one to zdarzenie lub zapisują się na nie.
  • typ delegate, który definiuje sygnaturę metod, które zostaną powiadomione o zdarzeniu

Framework .NET definiuje:

  • standardowy podpis zdarzenia o numerze delegate
public delegate void MyEventHandler(object source, EventArgs evtInfo);
  • source: obiekt, który zgłosił zdarzenie
  • evtInfo: obiekt typu EventArgs lub pochodny, zawierający informacje o zdarzeniu
  • nazwa obiektu typu delegate musi kończyć się na EventHandler
  • standardowy sposób deklaracji zdarzenia typu MyEventHandler w klasie:
1
2
3
4
public Class C1{
    public event MyEventHandler Evt1;
...
}

Pole Evt1 jest typu delegate. Słowo kluczowe event służy do ograniczenia operacji, które można na nim wykonywać:

  • spoza klasy C1 możliwe są wyłącznie operacje += i -=. Zapobiega to usunięciu (na przykład w wyniku pomyłki programisty) metod subskrybujących zdarzenie. Można po prostu subskrybować (+=) lub zrezygnować z subskrypcji (-=) zdarzenia.
  • Tylko instancja typu C1 może wykonać wywołanie Evt1(source,evtInfo), które uruchamia metody zapisane do zdarzenia Evt1.

Framework .NET udostępnia metodę generyczną zgodną z sygnaturą delegate zdarzenia:

public delegate void EventHandler<TEventArgs>(object source, TEventArgs evtInfo) where TEventArgs : EventArgs
  • delegate EventHandler wykorzystuje typ generyczny TEventArgs, który jest typem jego drugiego parametru
  • typ TEventArgs musi wywodzić się z typu EventsArgs (gdzie TEventArgs: EventArgs)

W przypadku tego ogólnego typu delegate deklaracja zdarzenia X w klasie C będzie zgodna z następującym zalecanym schematem:

  • zdefiniować typ XEventArgs wywodzący się z EventArgs w celu zawarcia informacji o zdarzeniu X
  • zdefiniować w klasie C zdarzenie typu EventHandler<XEventArgs>.
  • zdefiniować w klasie C metodę chronioną
protected void OnXHandler(XEventArgs e);

przeznaczoną do „opublikowania” zdarzenia X dla subskrybentów.

Rozważmy następujący przykład:

  • klasa Emetteur zawiera wartość temperatury. Temperatura ta jest monitorowana. Gdy przekroczy ona określony próg, należy wygenerować zdarzenie. Nazwiemy to zdarzenie TemperatureTropHaute. Informacje o tym zdarzeniu zostaną zawarte w typie TemperatureTropHauteEventArgs.
  • Klasa Souscripteur subskrybuje powyższe zdarzenie. Gdy zostanie powiadomiona o zdarzeniu, wyświetla komunikat na konsoli.
  • Program konsolowy tworzy nadawcę i dwóch subskrybentów. Wprowadza temperatury za pomocą klawiatury i zapisuje je w instancji typu Emetteur. Jeśli temperatura jest zbyt wysoka, instancja typu Emetteur publikuje zdarzenie TemperatureTropHaute.

Aby zachować zgodność z zalecaną metodą zarządzania zdarzeniami, najpierw definiujemy typ TemperatureTropHauteEventArgs w celu hermetyzacji informacji o zdarzeniu:


using System;

namespace Chap6 {
    public class TemperatureTropHauteEventArgs:EventArgs {
        // temperatura podczas zdarzenia
        public decimal Temperature { get; set; }
        // producenci
        public TemperatureTropHauteEventArgs() {
        }
        public TemperatureTropHauteEventArgs(decimal temperature) {
            Temperature = temperature;
        }
    }
}
  • wiersz 6: informacją zawartą w klasie TemperatureTropHauteEventArgs jest temperatura, która spowodowała zdarzenie TemperatureTropHaute.

Klasa Emetteur ma następującą postać:


using System;

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

        // temperatura zmierzona
        private decimal temperature;
        // nazwa źródła
        public string Nom { get; set; }
        // zgłoszone zdarzenie
        public event EventHandler<TemperatureTropHauteEventArgs> TemperatureTropHaute;

        // odczyt/zapis temperatury
        public decimal Temperature {
            get {
                return temperature;
            }
            set {
                temperature = value;
                if (temperature > SEUIL) {
                    // zgłoszenie zdarzenia do abonentów
                    OnTemperatureTropHaute(new TemperatureTropHauteEventArgs(temperature));
                }
            }
        }

        // zgłoszenie zdarzenia
        protected virtual void OnTemperatureTropHaute(TemperatureTropHauteEventArgs evt) {
            // wysyłanie zdarzenia TemperatureTropHaute do abonentów
            TemperatureTropHaute(this, evt);
        }
    }
}
  • wiersz 5: próg temperatury, powyżej którego zostanie opublikowane zdarzenie TemperatureTropHaute.
  • wiersz 10: nadawca posiada nazwę umożliwiającą jego identyfikację
  • wiersz 12: zdarzenie TemperatureTropHaute.
  • wiersze 15–26: metoda get, która zwraca temperaturę, oraz metoda set, która ją rejestruje. To metoda set powoduje opublikowanie zdarzenia TemperatureTropHaute, jeśli temperatura, która ma zostać zarejestrowana, przekroczy próg określony w wierszu 5. Metoda ta powoduje opublikowanie zdarzenia za pomocą metody OnTemperatureTropHauteHandler z wiersza 29, przekazując jej jako parametr obiekt TemperatureTropHauteEventArgs, w którym zapisano temperaturę, która przekroczyła próg.
  • wiersze 29–32: zdarzenie TemperatureTropHaute jest publikowane z pierwszym parametrem będącym samym nadawcą, a drugim parametrem obiektem TemperatureTropHauteEventArgs otrzymanym jako parametr.

Klasa Souscripteur, która będzie subskrybować zdarzenie TemperatureTropHaute, ma następującą postać:


using System;

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

        // osoba zarządzająca zdarzeniem TemperatureTropHaute
        public void EvtTemperatureTropHaute(object source, TemperatureTropHauteEventArgs e) {
            // wyświetlanie na konsoli operatora
            Console.WriteLine("Souscripteur [{0}] : la source [{1}] a signalé une température trop haute : [{2}]", Nom, ((Emetteur)source).Nom, e.Temperature);
        }
    }
}
  • wiersz 6: każdy subskrybent jest identyfikowany za pomocą nazwy.
  • wiersze 9–12: metoda, która zostanie powiązana ze zdarzeniem TemperatureTropHaute. Ma ona sygnaturę typu delegate EventHandler<TEventArgs>, którą musi posiadać menedżer zdarzeń. Metoda wyświetla na konsoli: nazwę subskrybenta wyświetlającego komunikat, nazwę nadawcy, który zgłosił zdarzenie, oraz temperaturę, która je wywołała.
  • Subskrypcja zdarzenia TemperatureTropHaute dla obiektu Emetteur nie jest realizowana w klasie Souscripteur. Zostanie ona zrealizowana przez klasę zewnętrzną.

Program [Program.cs] łączy wszystkie te elementy ze sobą:


using System;
namespace Chap6 {
    class Program {
        static void Main(string[] args) {
            // utworzenie nadajnika zdarzeń
            Emetteur e1 = new Emetteur() { Nom = "e" };
            // utworzenie tablicy zawierającej 2 subskrybentów
            Souscripteur[] souscripteurs = new Souscripteur[2];
            for (int i = 0; i < souscripteurs.Length; i++) {
                // utworzenie subskrybenta
                souscripteurs[i] = new Souscripteur() { Nom = "s" + i };
                // subskrybowanie zdarzenia TemperatureTropHaute z e1
                e1.TemperatureTropHaute += souscripteurs[i].EvtTemperatureTropHaute;
            }
            // odczyt temperatur z klawiatury
            decimal temperature;
            Console.Write("Température (rien pour arrêter) : ");
            string saisie = Console.ReadLine().Trim();
            // dopóki wprowadzony wiersz nie jest pusty
            while (saisie != "") {
                // czy wprowadzona wartość jest liczbą dziesiętną?
                if (decimal.TryParse(saisie, out temperature)) {
                    // temperatura poprawna – zapisujemy ją
                    e1.Temperature = temperature;
                } else {
                    // zgłaszamy błąd
                    Console.WriteLine("Température incorrecte");
                }
                // nowe wprowadzenie
                Console.Write("Température (rien pour arrêter) : ");
                saisie = Console.ReadLine().Trim();
            }//while
        }
    }
}
  • wiersz 6: utworzenie nadawcy
  • wiersze 8–14: utworzenie dwóch subskrybentów, których zapisano do zdarzenia TemperatureTropHaute nadawcy.
  • wiersze 20–32: pętla wprowadzania temperatur za pomocą klawiatury
  • wiersz 24: jeśli wprowadzona temperatura jest prawidłowa, jest ona przekazywana do obiektu Emetteur e1, który wywoła zdarzenie TemperatureTropHaute, jeśli temperatura przekracza 19 °C.

Wyniki wykonania są następujące:

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]