Skip to content

8. Події користувача

У попередньому розділі ми розглянули поняття подій, пов’язаних із компонентами форми. Тепер ми розглянемо, як створювати події у власних класах.

8.1. Заздалегідь визначені об’єкти-делегати

Поняття об’єкта delegate ми вже зустрічали в попередньому розділі, але тоді лише побіжно. Коли ми розглядали, як оголошуються обробники подій компонентів форми, ми натрапили на код, подібний до такого:


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

де buttonAfficher був компонентом типу [Button]. Цей клас має поле Click, визначене наступним чином:

  • [1]: клас [Button]
  • [2]: його події
  • [3,4]: подія Click
  • [5]: оголошення події [Control.Click] [4].
    • EventHandler — це прототип (модель) методу, який називається делегатом.
    • event — це ключове слово, яке обмежує функціональні можливості delegate та EventHandler: об’єкт delegate має ширші можливості, ніж об’єкт event.

delegate EventHandler визначається наступним чином:

 

delegate EventHandler позначає шаблон методу:

  • перший параметр якої — тип Object
  • має другим параметром тип EventArgs
  • який не повертає жодного результату

Метод, що відповідає моделі, визначеній EventHandler, може виглядати так:


        private void buttonAfficher_Click(object sender, EventArgs e);

Щоб створити об’єкт типу EventHandler, слід виконати такі дії:

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

Таким чином можна записати:

EventHandler evtHandler=new EventHandler(buttonAfficher_Click);

Змінна типу delegate — це, по суті, список посилань на методи, що відповідають шаблону delegate. Щоб додати новий метод M до наведеної вище змінної evtHandler, використовується такий синтаксис:

evtHandler+=new EvtHandler(M);

Запис += можна використовувати навіть у тому випадку, якщо evtHandler є порожнім списком.

Інструкція:


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

додає метод типу EventHandler до списку методів події buttonAfficher.Click. Коли відбувається подія Click на компоненті buttonAfficher, VB виконує інструкцію:

            buttonAfficher.Click(source, evt);

де:

  • source — це компонент типу object, що ініціював подію
  • evt типу EventArgs і не містить інформації

Усі методи підпису void M(object,EventArgs), які були пов’язані з подією Click за допомогою:


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

будуть викликані з параметрами (source, evt), переданими VB.

8.2. Визначення об’єктів-делегатів

Інструкція


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

визначає тип з назвою Opération, який представляє прототип функції, що приймає два цілі числа та повертає ціле число. Саме ключове слово delegate робить Opération визначенням прототипу функції.

Змінна op типу Opération слугуватиме для зберігання списку функцій, що відповідають прототипу Opération:

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

Запис методу fi у змінну op здійснюється за допомогою op=new Операція(fi) або, простіше, за допомогою op=fi. Щоб додати метод fj до списку вже зареєстрованих функцій, записуємо op+= fj. Щоб видалити вже зареєстрований метод fk, записуємо op-=fk. Якщо в нашому прикладі написати n=op(n1,n2), то всі методи, зареєстровані у змінній op, будуть виконані з параметрами n1 та n2. Отриманий результат n буде результатом останнього виконаного методу. Отримати результати, згенеровані всіма методами, неможливо. З цієї причини, якщо список методів зберігається у делегованій функції, вони найчастіше повертають результат типу void.

Розглянемо такий приклад:


using System;
namespace Chap6 {
    class Class1 {
        // визначення прототипу функції
        // приймає 2 цілі числа як параметри та повертає ціле число
        public delegate int Opération(int n1, int n2);

        // два методи екземпляра, що відповідають прототипу
        public int Ajouter(int n1, int n2) {
            Console.WriteLine("Ajouter(" + n1 + "," + n2 + ")");
            return n1 + n2;
        }//додати

        public int Soustraire(int n1, int n2) {
            Console.WriteLine("Soustraire(" + n1 + "," + n2 + ")");
            return n1 - n2;
        }//віднімати

        //— один статичний метод, що відповідає прототипу
        public static int Augmenter(int n1, int n2) {
            Console.WriteLine("Augmenter(" + n1 + "," + n2 + ")");
            return n1 + 2 * n2;
        }//збільшити

        static void Main(string[] args) {

            // визначаємо об’єкт типу «операція» для зберігання функцій
            // записуємо статичну функцію «збільшити»
            Opération op = Augmenter;
            // виконується делегат
            int n = op(4, 7);
            Console.WriteLine("n=" + n);

            // створення об’єкта c1 типу class1
            Class1 c1 = new Class1();
            // у делегаті реєструється метод «додати» об’єкта c1
            op = c1.Ajouter;
            // виконання об’єкта-делегата
            n = op(2, 3);
            Console.WriteLine("n=" + n);
            // запис методу «віднімати» об’єкта c1 у делегат
            op = c1.Soustraire;
            n = op(2, 3);
            Console.WriteLine("n=" + n);
            //запис двох функцій у делегат
            op = c1.Ajouter;
            op += c1.Soustraire;
            // виконання делегованого об’єкта
            op(0, 0);
            // вилучення функції з делегату
            op -= c1.Soustraire;
            // виконується делегат
            op(1, 1);
        }
    }
}
  • рядок 3: визначає клас Class1.
  • рядок 6: визначення delegate Opération: прототип методів, що приймають два параметри типу int і повертають результат типу int
  • рядки 9–12: метод екземпляра Ajouter має сигнатуру делегату Opération.
  • рядки 14–17: метод екземпляра Soustraire має сигнатуру делегату Opération.
  • рядки 20–23: метод класу Augmenter має сигнатуру делегату Opération.
  • рядок 25: метод Main, що виконується
  • рядок 20: змінна op має тип делегату «Операція». Вона міститиме список методів, що мають сигнатуру типу делегату «Операція». Їй присвоюється перше посилання на метод — на статичний метод Class1.Augmenter.
  • рядок 31: виконується delegate op: будуть виконані всі методи, на які посилається op. Вони будуть виконані з параметрами, переданими методам delegate та op.. У цьому випадку буде виконано лише статичний метод Class1.Augmenter.
  • рядок 35: створюється екземпляр c1 класу Class1.
  • рядок 37: метод екземпляра c1.Ajouter присвоюється делегату op. Augmenter був статичним методом, Ajouter — методом екземпляра. Ми хотіли показати, що це не має значення.
  • рядок 39: виконується delegate op: метод Ajouter буде виконано з параметрами, переданими до delegate op.
  • рядок 42: те саме повторюється з методом екземпляра Soustraire.
  • рядки 46–47: методи Ajouter та Soustraire вкладаються в delegate та op.
  • рядок 49: виконується delegate op: обидва методи Ajouter та Soustraire будуть виконані з параметрами, переданими до delegate та op.
  • рядок 51: метод Soustraire видалено з delegate та op.
  • рядок 53: виконується delegate op: залишився метод Ajouter, який буде виконано.

Результати виконання такі:

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 та інтерфейсів можуть здаватися досить схожими, і можна задатися питанням, у чому саме полягають відмінності між цими двома поняттями. Розглянемо наступний приклад, схожий на вже розглянутий:


using System;
namespace Chap6 {
    class Program1 {
        // визначення прототипу функції
        // приймає 2 цілі числа як параметри та повертає ціле число
        public delegate int Opération(int n1, int n2);

        // два методи екземпляра, що відповідають прототипу
        public static int Ajouter(int n1, int n2) {
            Console.WriteLine("Ajouter(" + n1 + "," + n2 + ")");
            return n1 + n2;
        }//додати

        public static int Soustraire(int n1, int n2) {
            Console.WriteLine("Soustraire(" + n1 + "," + n2 + ")");
            return n1 - n2;
        }//віднімати

        // Виконання делегату
        public static int Execute(Opération op, int n1, int n2){
            return op(n1, n2);
        }

        static void Main(string[] args) {
            // виконання делегату «Додати»
            Console.WriteLine(Execute(Ajouter, 2, 3));
            // виконання делегату «Відняти»
            Console.WriteLine(Execute(Soustraire, 2, 3));
            // виконання делегату мультикасту
            Opération op = Ajouter;
            op += Soustraire;
            Console.WriteLine(Execute(op, 2, 3));
            // Вилучення функції з делегата
            op -= Soustraire;
            // виконання делегату
            Console.WriteLine(Execute(op, 2, 3));
        }
    }
}

У рядку 20 метод Execute очікує посилання на об’єкт типу делегата Opération, визначеного в рядку 6. Це дозволяє передавати до методу Execute різні методи (рядки 26, 28, 32 та 36). Цю властивість поліморфізму можна також отримати за допомогою інтерфейсу:


using System;

namespace Chap6 {

    // інтерфейс IOperation
    public interface IOperation {
        int operation(int n1, int n2);
    }

    // клас «Додати»
    public class Ajouter : IOperation {
        public int operation(int n1, int n2) {
            Console.WriteLine("Ajouter(" + n1 + "," + n2 + ")");
            return n1 + n2;
        }
    }

    // клас «Віднімати»
    public class Soustraire : IOperation {
        public int operation(int n1, int n2) {
            Console.WriteLine("Soustraire(" + n1 + "," + n2 + ")");
            return n1 - n2;
        }
    }

    // тестовий клас
    public static class Program2 {
        // Виконання єдиного методу інтерфейсу IOperation
        public static int Execute(IOperation op, int n1, int n2) {
            return op.operation(n1, n2);
        }

        public static void Main() {
            // виконання делегату «Додати»
            Console.WriteLine(Execute(new Ajouter(), 2, 3));
            // виконання делегату «Відняти»
            Console.WriteLine(Execute(new Soustraire(), 2, 3));
        }
    }
}
  • рядки 6–8: інтерфейс [IOperation] визначає метод operation.
  • рядки 11–16 та 19–24: класи [Ajouter] та [Soustraire] реалізують інтерфейс [IOperation].
  • рядки 29–31: метод Execute, перший параметр якого має тип інтерфейсу IOperation. Метод Execute послідовно отримає як перший параметр екземпляр класу Ajouter, а потім — екземпляр класу Soustraire.

Тут добре простежується поліморфізм, який мав параметр типу delegate у попередньому прикладі. Обидва приклади водночас демонструють відмінності між цими двома поняттями.

Типи delegate та interface є взаємозамінними

  • , якщо інтерфейс має лише один метод. Дійсно, тип delegate є оболонкою для єдиного методу, тоді як інтерфейс може визначати кілька методів.
  • якщо багатоадресний аспект delegate не використовується. Адже поняття багатоадресності в інтерфейсі не існує.

Якщо ці дві умови виконуються, то для методу Execute можна вибрати одну з двох наступних сигнатур:


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

Друга сигнатура, яка використовує delegate, може виявитися більш гнучкою у використанні. Адже у першій сигнатурі перший параметр методу повинен реалізовувати інтерфейс IOperation. Це змушує створювати клас для визначення методу, який має передаватися як перший параметр методу Execute. У другому сигнатурі підійде будь-який існуючий метод із відповідним сигнатурою. Додаткових конструкцій створювати не потрібно.

8.4. Обробка подій

Об’єкти delegate можна використовувати для визначення подій. Клас C1 може визначити подію evt таким чином:

  • тип delegate визначається всередині або поза класом C1:
delegate TResult Evt(T1 param1, T2 param2, ...);
  • клас C1 визначає поле типу delegate Evt:
public Evt Evt1;
  • коли екземпляр c1 класу C1 захоче повідомити про подію, він виконає свій делегат Evt1, передавши йому параметри, визначені делегатом Evt. Усі методи, зареєстровані в delegate та Evt1, будуть тоді виконані з цими параметрами. Можна сказати, що вони були сповіщені про подію Evt1.
  • якщо об’єкт c2, який використовує об’єкт c1, хоче отримати повідомлення про виникнення події Evt1 на об’єкті c1, він зареєструє один зі своїх методів c2.M у делегованому об’єкті c1.Evt1 об’єкта c1.. Таким чином, його метод c2.M виконуватиметься щоразу, колиподія Evt1 відбуватиметься на об’єкті c1. Він також зможе скасувати підписку, коли більше не захоче отримувати сповіщення про цю подію.
  • Оскільки об’єкт-делегат c1.Evt1 може зареєструвати кілька методів, то різні об’єкти ci зможуть зареєструватися у делегата c1.Evt1, щоб отримувати сповіщення про подію Evt1 на об’єкті c1.

У цьому сценарії маємо:

  • клас, який сигналізує про подію
  • класи, які отримують повідомлення про цю подію. Кажуть, що вони підписуються на подію або підписуються на подію.
  • тип delegate, який визначає сигнатуру методів, що отримуватимуть сповіщення про подію

Фреймворк .NET визначає:

  • стандартний підпис події delegate
public delegate void MyEventHandler(object source, EventArgs evtInfo);
  • source: об’єкт, який повідомив про подію
  • evtInfo: об’єкт типу EventArgs або його похідний, що надає інформацію про подію
  • ім'я delegate має закінчуватися на EventHandler
  • стандартний спосіб оголошення події типу MyEventHandler у класі:
1
2
3
4
public Class C1{
    public event MyEventHandler Evt1;
...
}

Поле Evt1 має тип delegate. Ключове слово event призначене для обмеження операцій, які можна виконувати над ним:

  • з-поза меж класу C1 можливі лише операції += та -=. Це запобігає видаленню (наприклад, через помилку розробника) методів, підписаних на подію. Можна просто підписатися (+=) або відписатися (-=) від події.
  • лише екземпляр типу C1 може виконати виклик Evt1(source,evtInfo), який запускає виконання методів, підключених до події Evt1.

Фреймворк .NET надає загальний метод, що відповідає сигнатурі delegate події:

public delegate void EventHandler<TEventArgs>(object source, TEventArgs evtInfo) where TEventArgs : EventArgs
  • delegate EventHandler використовує узагальнений тип TEventArgs, який є типом його другого параметра
  • тип TEventArgs повинен походити від типу EventsArgs (де TEventArgs : EventArgs)

З цим загальним типом delegate оголошення події X у класі C відповідатиме такому рекомендованому схемі:

  • визначити тип XEventArgs, похідний від EventArgs, для інкапсуляції інформації про подію X
  • визначити у класі C подію типу EventHandler<XEventArgs>.
  • визначити у класі C захищений метод
protected void OnXHandler(XEventArgs e);

, призначений для «публікації» події X передплатникам.

Розглянемо такий приклад:

  • клас Emetteur інкапсулює температуру. Ця температура відстежується. Коли температура перевищує певний поріг, має бути запущена подія. Назвемо цю подію TemperatureTropHaute. Інформація про цю подію буде інкапсульована в тип TemperatureTropHauteEventArgs.
  • Клас Souscripteur підписується на попередню подію. Коли він отримує сповіщення про подію, він виводить повідомлення на консоль.
  • Консольна програма створює один вивідник і двох підписників. Вона вводить температури з клавіатури та записує їх у екземпляр типу Emetteur. Якщо температура занадто висока, екземпляр типу Emetteur публікує подію TemperatureTropHaute.

Щоб дотриматися рекомендованого методу обробки подій, спочатку ми визначаємо тип TemperatureTropHauteEventArgs для інкапсуляції інформації про подію:


using System;

namespace Chap6 {
    public class TemperatureTropHauteEventArgs:EventArgs {
        // температура під час події
        public decimal Temperature { get; set; }
        // виробники
        public TemperatureTropHauteEventArgs() {
        }
        public TemperatureTropHauteEventArgs(decimal temperature) {
            Temperature = temperature;
        }
    }
}
  • рядок 6: інформація, інкапсульована класом TemperatureTropHauteEventArgs, — це температура, яка спричинила подію TemperatureTropHaute.

Клас Emetteur має такий вигляд:


using System;

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

        // виміряна температура
        private decimal temperature;
        // назва джерела
        public string Nom { get; set; }
        // подія зареєстрована
        public event EventHandler<TemperatureTropHauteEventArgs> TemperatureTropHaute;

        // зчитування/запис температури
        public decimal Temperature {
            get {
                return temperature;
            }
            set {
                temperature = value;
                if (temperature > SEUIL) {
                    // повідомлення про подію абонентам
                    OnTemperatureTropHaute(new TemperatureTropHauteEventArgs(temperature));
                }
            }
        }

        // повідомлення про подію
        protected virtual void OnTemperatureTropHaute(TemperatureTropHauteEventArgs evt) {
            // передача події TemperatureTropHaute абонентам
            TemperatureTropHaute(this, evt);
        }
    }
}
  • рядок 5: поріг температури, при перевищенні якого буде опубліковано подію TemperatureTropHaute.
  • рядок 10: відправник має ім’я для ідентифікації
  • рядок 12: подія TemperatureTropHaute.
  • рядки 15–26: метод get, який виводить температуру, та метод set, який її записує. Саме метод set забезпечує публікацію події TemperatureTropHaute, якщо температура, що підлягає реєстрації, перевищує поріг, зазначений у рядку 5. Вона публікує подію за допомогою методу OnTemperatureTropHauteHandler з рядка 29, передаючи йому як параметр об’єкт TemperatureTropHauteEventArgs, у якому записано температуру, що перевищила поріг.
  • рядки 29–32: подія TemperatureTropHaute публікується з першим параметром — самим відправником — та другим параметром — об’єктом TemperatureTropHauteEventArgs, отриманим як параметр.

Клас Souscripteur, який буде підписуватися на подію TemperatureTropHaute, має такий вигляд:


using System;

namespace Chap6 {
    public class Souscripteur {
        // назва
        public string Nom { get; set; }

        // адміністратор події TemperatureTropHaute
        public void EvtTemperatureTropHaute(object source, TemperatureTropHauteEventArgs e) {
            // відображення на консолі оператора
            Console.WriteLine("Souscripteur [{0}] : la source [{1}] a signalé une température trop haute : [{2}]", Nom, ((Emetteur)source).Nom, e.Temperature);
        }
    }
}
  • рядок 6: кожен підписник ідентифікується за іменем.
  • рядки 9–12: метод, який буде пов’язаний із подією TemperatureTropHaute. Вона має сигнатуру типу delegate EventHandler<TEventArgs>, яку повинен мати обробник події. Метод виводить на консоль: ім’я підписника, який відображає повідомлення; ім’я відправника, який повідомив про подію; температуру, що спричинила цю подію.
  • Підписка на подію TemperatureTropHaute об’єкта Emetteur не здійснюється у класі Souscripteur. Вона буде здійснена зовнішнім класом.

Програма [Program.cs] пов’язує всі ці елементи між собою:


using System;
namespace Chap6 {
    class Program {
        static void Main(string[] args) {
            // створення генератора подій
            Emetteur e1 = new Emetteur() { Nom = "e" };
            // створення масиву з 2 підписників
            Souscripteur[] souscripteurs = new Souscripteur[2];
            for (int i = 0; i < souscripteurs.Length; i++) {
                // створення підписника
                souscripteurs[i] = new Souscripteur() { Nom = "s" + i };
                // підписка на подію TemperatureTropHaute з e1
                e1.TemperatureTropHaute += souscripteurs[i].EvtTemperatureTropHaute;
            }
            // зчитування температур з клавіатури
            decimal temperature;
            Console.Write("Température (rien pour arrêter) : ");
            string saisie = Console.ReadLine().Trim();
            // доки введений рядок не порожній
            while (saisie != "") {
                // чи є введене значення десятковим числом?
                if (decimal.TryParse(saisie, out temperature)) {
                    // температура правильна — її записують
                    e1.Temperature = temperature;
                } else {
                    // повідомляється про помилку
                    Console.WriteLine("Température incorrecte");
                }
                // нове введення
                Console.Write("Température (rien pour arrêter) : ");
                saisie = Console.ReadLine().Trim();
            }//while
        }
    }
}
  • рядок 6: створення емітента
  • рядки 8–14: створення двох підписників, яких підписують на подію TemperatureTropHaute від джерела.
  • рядки 20–32: цикл введення температур з клавіатури
  • рядок 24: якщо введена температура правильна, вона передається об’єкту Emetteur e1, який запустить подію TemperatureTropHaute, якщо температура перевищує 19 °C.

Результати виконання такі:

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]