Опубликован: 25.03.2010 | Уровень: для всех | Доступ: платный
Лекция 10:

Интерфейсы, делегаты, события в C#

Создание событий с контролем адресатов

Если мы только используем библиотечные классы, содержащие события, то нам вообще никакой способ создания событий знать необязатально. Уметь подписывать в клиенте обработчики на уже готовые события и понимать причины, приводящие к возникновению этих событий, - это все, что нам необходимо.

Если мы хотим как-то повлиять на событие библиотечного класса, то нужно помнить, что событие объявляется общедоступными и может наследоваться как обычный член класса. Это дает нам право расширить библиотечный класс, в котором можно выполнить какие-то действия: скрыть событие, объявив его заново с ключевым словом new как закрытое, или переопределить диспетчер события. Диспетчер в библиотечных классах обычно объявляется виртуальным и может быть переопределен в производном классе или даже совсем скрыт для следующих потомков его переобъявлением.

Независимо от того, создаем ли мы свои классы с событиями, или расширяем библиотечные классы, добавляя в них новые события, о существовании еще одного способа знать не помешает.

Рассмотренный ранее способ создания объявления событий в классах-источниках считается стандартным. Мы создали таким способом класс с событием и теперь этим событием может пользоваться кто угодно, присоединяя к нему любые обработчики, лишь бы они имели разрешенный прототип. Такой способ не позволяет программисту осуществлять должный контроль за обработчиками при их добавлении и изъятии, как это может потребоваться в некоторых случаях. Для решения подобных задач существует другой способ создания события в классе-источнике, который является более гибким и его называют расширенным. Этот способ позволяет отслеживать в классе, содержащем событие, те обработчики, которые клиентский код будет присоединять к нему.

Синтаксис создания события расширенным способом сильно напоминает создание в классе свойства. Он также основывается на закрытом базовом поле и также обертывает это поле общедоступным членом. Только вместо автоматически вызываемых аксессоров get и set используются ключевые слова add и remove, а вместо базового поля используется закрытое поле-делегат. Методы add и remove автоматически вызываются при добавлении обработчика в список делегата или удаления его из списка.

Пусть мы создаем класс с событием и не хотим, чтобы какой-то определенный клиентский класс пользовался этим событием, т.е. один тип не хочет обслуживать другой неприятный ему тип. Тогда код создания события мог бы выглядеть так, как показано в следующем примере

using System;
    
namespace Test
{
    // Объявление делегата как типа 
    delegate void Message(object sender, string message);
    
    // Класс-источник сообщения
    class SourceMessage
    {
        ////////////////////////////////////////////
        // Это был стандартный способ
        ////////////////////////////////////////////
        // public event Message Mail;
    
        ////////////////////////////////////////////
        // Это расширенный способ
        ////////////////////////////////////////////
        // Объявление внутреннего поля как экземпляра делегата
        private Message mail;
    
        // Создание события с контролем адресатов
        public event Message Mail
        {
            add     // Контролируем добавление обработчиков в список
            {
                // Имя получателя сообщения
                string targetName = value.Target.GetType().Name;
    
                // Выявляем того, кого не любим!
                if (targetName == "BadClass")
                    // Ласково уведомляем 
                    Console.WriteLine("Типу BadClass 
      доступ к событию запрещен!\n");
                else
                    mail += value;
            }
            remove  // Удаление контролировать не будем. 
         //"Его там не стояло!" 
            {
                mail -= value;
            }
        }
    
        /////////////////////////////////////////////
        // Все остальное то-же самое, кроме замены
        // события Mail на закрытое поле-делегат mail
        /////////////////////////////////////////////
        // Метод диспетчеризации события. Объявили виртуальным 
        // и защищенным для возможности замещения в наследниках
        protected virtual void OnMail(string mess)
        {
            // Инициируем рассылку сообщения всем, 
            // кто подписался на событие
            // Здесь используется поле-делегат, внутри можно (и нужно!)
            if (mail != null)       // Если не пустой делегат
                mail(this, mess);   // Инициируем событие
        }
    
        // Объявляем и инициируем внутреннее поле базовым сообщением
        string message = "Сообщаю, что сработал таймер!!!\n"
            + "Текущее время ";
    
        // Объявляем внутреннее поле для видимости в методах
        System.Timers.Timer timer;
    
        // Конструктор класса-источника сообщения
        public SourceMessage()
        {
            // Создаем и запускаем таймер, который по истечении 
            // заданного времени инициирует рассылку сообщения
            timer = new System.Timers.Timer();
            timer.Interval = 5000d; // Сработает через 5 секунд
            timer.Elapsed += new System.Timers.ElapsedEventHandler
    (timer_Elapsed);
            timer.Start();      // Запускаем таймер
        }
    
        // Инициирование события из внешнего источника
        void timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
        {
            // Извлекаем текущее системное время
            DateTime curTime = DateTime.Now;
            // Дополняем базовое сообщение временем
            message += curTime.ToString();
    
            OnMail(message);    // Вызываем метод диспетчеризации
            timer.Stop();       // Останавливаем системный таймер
        }
    }
    
    // Хороший класс-получатель сообщения
    class GoodClass
    {
        public GoodClass()
        {
            // Создаем объект с событием
            SourceMessage obj = new SourceMessage();
    
            // Подписываем на событие обработчик,
            // печатающий полученную информацию
            obj.Mail += new Message(obj_Mail);
        }
    
        // Обработчик
        void obj_Mail(object sender, string message)
        {
            String str = this.GetType().Name
                + " получил информацию от "
                + sender.GetType().Name + "\n"
                + "следующего содержания:\n\"" 
     + message + "\"";
            Console.WriteLine(str);
        }
    }
    
    // Плохой класс, которому не разрешено получать сообщения
    class BadClass
    {
        public BadClass()
        {
            // Создаем объект с событием
            SourceMessage obj = new SourceMessage();
    
            // Подписываемся на событие в благостном неведении,
            // что мы попали в черный список и нас
            // не включат в список рассылки
            obj.Mail += new Message(obj_Mail);
        }
    
        // Обработчик
        void obj_Mail(object sender, string message)
        {
            String str = "Получена информация от класса "
                + sender.GetType().Name + ":\n"
                + "\"" + message + "\"";
            Console.WriteLine(str);
        }
    }
    
    // Запуск
    class Program
    {
        static void Main()
        {
            // Настройка консоли
            Console.Title = "Расширенное создание 
    события (через 5 секунд!)";
    
            Console.ForegroundColor = ConsoleColor.White;
            Console.CursorVisible = false;
            Console.WindowWidth = 47;
            Console.WindowHeight = 7;
    
            new GoodClass();// Исполняем хороший класс
            new BadClass(); // Исполняем плохой класс
    
            Console.ReadLine();
        }
    }
}
Листинг 10.20 . Создание события с контролирующим подписку кодом

Из распечатки мы видим, что хотя событие класса GoodClass и было инициировано первым, но отработало позднее, поскольку ждало срабатывания системного таймера.

Создание событий со списком делегатов

В библиотеке .NET Framework для структурированной поддержки событий создан класс EventHandlerList, который может хранить в себе элементы, выполняющие функцию делегатов с присоединенными к ним обработчиками. Эти элементы маркируются ключами типа Object. Каждому ключу соответствует один или несколько одноадресных делегатов, способных хранить вызовы обработчиков одного и того же прототипа.

За событием закрепляется определенный ключ и при манипуляции с событием во внешнем коде делегаты, маркированные этим ключом, добавляются в список. Чтобы активизировать событие, достаточно обратиться к списку с закрепленным за событием ключом, все одноадресные делегаты будут извлечены из списка и присоединенные обработчики будут выполнены.

Поскольку элементы списка реализуют вызов обработчиков одноадресным способом, то использование списка позволяет прикреплять обработчики не только с пустым возвращаемым значением, но и с возвращаемым значением любого типа. Хотя в большинстве случаев это несущественное преимущество, поскольку результат обработки можно вернуть и через аргументы обработчика. Класс EventHandlerList находится в пространстве имен System. ComponentModel. В следующем примере демонстрируется создание событий на основе списка одноадресных делегатов.

using System;
    
namespace Test
{
    // Класс-источник сообщения
    class SourceMessage
    {
        // Создание списка делегатов вместо базовых полей
        System.ComponentModel.EventHandlerList eventList
            = new System.ComponentModel.EventHandlerList();
    
        // Объявление типов делегатов
        public delegate void Message1();
        public delegate int Message2(string message);
        // Непустое возвращаемое значение
        public delegate void Message3(object sender, string message);
    
        // Создание ключей для делегатов
        Object key1 = new Object();
        Object key2 = new Object();
        Object key3 = new Object();
    
        // Создание события на базе списка
        public event Message1 Mail1
        {
            add
            {
                eventList.AddHandler(key1, value);// Дополняем список делегатов
            }
            remove
            {
                eventList.RemoveHandler(key1, value);// Удаляем из списка
            }
        }
    
        // Создание события на базе списка
        public event Message2 Mail2
        {
            add
            {
                eventList.AddHandler(key2, value);// Расширяем список делегатов
            }
            remove
            {
                eventList.RemoveHandler(key2, value);// Удаляем из списка
            }
        }
    
        // Создание события на базе списка
        public event Message3 Mail3
        {
            add
            {
                eventList.AddHandler(key3, value);// Расширяем список делегатов
            }
            remove
            {
                eventList.RemoveHandler(key3, value);// Удаляем из списка
            }
        }
    
        // Симуляция срабатывания события Mail1
        public void DispatchMail1()
        {
            // Извекаем из списка все делегаты для Mail1, помеченные ключом
            Message1 mail1 = (Message1)eventList[key1];
            if (mail1 != null)
                mail1();
        }
    
        // Симуляция срабатывания события Mail2
        public void DispatchMail2()
        {
            // Извекаем из списка все делегаты для Mail2, помеченные ключом
            Message2 mail2 = (Message2)eventList[key2];
            if (mail2 != null)
                mail2("\"mail2 из SourceMessage\"");
        }
    
        // Симуляция срабатывания события Mail3
        public void DispatchMail3()
        {
            // Извекаем из списка все делегаты для Mail3, помеченные ключом
            Message3 mail3 = (Message3)eventList[key3];
            if (mail3 != null)
                mail3(this, "\"mail3 из SourceMessage\"");
        }
    }
    
    // Получатель сообщения
    class MyClass
    {
        // Конструктор
        public MyClass()
        {
            // Создаем объект с событиями
            SourceMessage obj = new SourceMessage();
    
            // Подписываемся на обработчики
            obj.Mail1 += new SourceMessage.Message1(obj_Mail1);
            obj.Mail1 += new SourceMessage.Message1(obj_Mail1);
            obj.Mail2 += new SourceMessage.Message2(obj_Mail2);
            obj.Mail3 += new SourceMessage.Message3(obj_Mail3);
    
            // Запускаем события
            obj.DispatchMail1();
            obj.DispatchMail2();
            obj.DispatchMail3();
        }
    
        // Обработчики
        void obj_Mail1()
        {
            Console.WriteLine("Обработчик события Mail1.");
        }
    
        int obj_Mail2(string message)
        {
            Console.WriteLine("\nОбработчик события Mail2.\n" +
                "Сообщение: {0}\n", message);
            return 1;
        }
    
        void obj_Mail3(object sender, string message)
        {
            Console.WriteLine("Обработчик события Mail3.\n" +
                "Сообщение: {0}", message);
        }
    }
    
    // Запуск
    class Program
    {
        static void Main()
        {
            // Настройка консоли
            Console.Title = "Применение списка делегатов";
    
            Console.ForegroundColor = ConsoleColor.White;
            Console.CursorVisible = false;
            Console.WindowWidth = 36;
            Console.WindowHeight = 9;
    
            new MyClass();// Исполняем
    
            Console.ReadLine();
        }
    }
}
Листинг 10.21 . Создание событий со списком делегатов

Максим Филатов
Максим Филатов

Прошел курс. Получил код Dreamspark. Ввожу код на сайте, пишет:

Срок действия этого кода проверки уже истек. Проверьте, правильно ли введен код. У вас осталось две попытки. Вы также можете выбрать другой способ проверки или предоставить соответствующие документы, подтверждающие ваш академический статус.

 

Как активировать код?

Денис Пашков
Денис Пашков
Россия
Татьяна Ковалюк
Татьяна Ковалюк
Украина, Киев, Киевский политехнический институт, 1974