• Microsoft .NET
  • C#.NET
  • Возбуждение событий, обработчики событий и использование делегатов

Возбуждение событий, обработчики событий и использование делегатов

ОГЛАВЛЕНИЕ

Делегаты и их связь с событиями и обработчиками событий.

•    Скачать исходники - 20.4 Кб

События

При работе с IDE (интегрированная среда разработки), такой как Visual Studio, большинство событий, особенно касающихся приложений Windows Forms, являются нелинейными. То есть приходится ждать, когда пользователь кликнет по кнопке или нажмет кнопку клавиатуры, чтобы затем отреагировать на это событие. В серверных приложениях приходится ждать (и слушать) входящий сетевой запрос. Эти возможности предоставляются событиями в каркасе .NET. Любой разработчик заметит связь между событиями и делегатами в технической документации. Данная статья предполагает знания .NET Framework 2.0 и Visual Studio 2005 (2008), с целью определения событий и делегатов. Хотя эти темы рассматриваются отдельно, они взаимосвязаны и могут быть объединены для достижения лучшего понимания общего принципа вызова метода при событии или ссылке на этот метод.

Первый раздел разбирает события, их устройство и возможности при их определении в классе. Следующий раздел разбирает делегаты, их устройство, как они объявляются, инстанцируются и используются. Читатель должен заметить, что основная идея – вызов метода для выполнения действия в зависимости от события, и метод, вызываемый по ссылке на этот метод, отличной от указателя функции.

Класс может определять такой вид члена, как событие. Тип, определяющий член-событие, позволяет этому типу (или экземплярам этого типа (класса)) извещать другие объекты, что произошло нечто особенное. Например, класс Button предоставляет событие под именем Click. Когда по объекту Button(кнопка) щелкают мышкой, один или более объектов в приложении могут хотеть получить извещение об этом событии, чтобы выполнить некое действие. События являются типами, обеспечивающими данное взаимодействие. Определение члена-события означает, что класс предоставляет следующие возможности:
•    Статический метод типа или метод экземпляра объекта могут регистрировать свой интерес к событию типа.
•    Статический метод типа или метод экземпляра объекта могут отменять регистрацию своего интереса к событию типа.
•    Зарегистрированные методы будут оповещены при наступлении события

Типы могут оказывать данные функции при определении события, потому что они хранят список зарегистрированных методов. Когда событие наступает, тип оповещает все зарегистрированные методы в списке. Модель событий общеязыковой среды исполнения основана на делегатах. Делегат – безопасный по типу способ вызова методов обратного вызова. Методы обратного вызова являются средствами, путем которых объекты получают оповещения, на которые подписаны.

Событие является сообщением, отправляемым объектом для оповещения об осуществлении действия. Действие может вызываться взаимодействием с пользователем, таким как нажатие кнопки мыши, или же событие может запускаться иной программной логикой. Объект, возбуждающий событие, называется источником события. Объект, перехватывающий и реагирующий на событие, называется получателем события. При передаче события класс источника события не знает, какой объект или метод получит (или обработает) возбужденное им событие. Необходим посредник (или механизм наподобие указателя) между источником и получателем. То есть объект, получающий событие, не может проверить источник события. Каркас .NET определяет специальный тип (делегат), служащий указателем функции. Код ниже показывает пример события и вводит делегаты. Читатель не обязан сразу понять принцип работы кода, так как он объясняется в следующих разделах статьи.

using System;
using System.ComponentModel;
namespace EventSample

   using System;
   using System.ComponentModel;

   // Класс, содержащий данные для
   // события сигнала. Унаследован от System.EventArgs.
   //
   public class AlarmEventArgs : EventArgs
   { 
      private readonly bool snoozePressed ;
      private readonly int nrings;
     
      //Конструктор.
      //
      public AlarmEventArgs(bool snoozePressed, int nrings)
      {
         this.snoozePressed = snoozePressed;
         this.nrings = nrings;
      }

      // Свойство NumRings возвращает количество звонков,
      // которые будильник издает при генерации
      // события сигнала.
      //
      public int NumRings
      {    
         get { return nrings;}     
      }

      // Свойство SnoozePressed показывает, нажата ли кнопка повторения сигнала
      // при генерации события сигнала.
      //
      public bool SnoozePressed
      {
         get {return snoozePressed;}
      }

      // Свойство AlarmText, содержащее пробуждающее сообщение.
      //
      public string AlarmText
      {
         get
         {
            if (snoozePressed)
            {
               return ("Проснитесь!!! Время сна закончилось.");
            }
            else
            {
               return ("Wake Up!");
            }
         }
      } 
   }

   // Объявление делегата.
   //
   public delegate void AlarmEventHandler(object sender, AlarmEventArgs e);

   // Класс будильника, возбуждающий событие сигнала.
   //
   public class AlarmClock
   { 
      private bool snoozePressed = false;
      private int nrings = 0;
      private bool stop = false;

      // Свойство Stop(остановить) показывает, должен ли
      // сигнал отключаться.
      //
      public bool Stop
      {
         get {return stop;}
         set {stop = value;}
      }

      // Свойство SnoozePressed показывает, нажата ли кнопка повторения сигнала
      // при генерации события сигнала.
      //
      public bool SnoozePressed
      {
         get {return snoozePressed;}
         set {snoozePressed = value;}
      }
      // Член события имеет тип AlarmEventHandler.
      //
      public event AlarmEventHandler Alarm;

      // Защищенный метод OnAlarm возбуждает событие путем вызова
      // делегатов. Источником всегда является this, текущий экземпляр класса.
      //
      protected virtual void OnAlarm(AlarmEventArgs e)
      {
         if (Alarm != null)
         {
            // Вызывает делегаты.
            Alarm(this, e);
         }
      }

      // Данный будильник не имеет
      // пользовательского интерфейса.
      // Механизм включения сигнала моделируется с помощью цикла,
      // возбуждающего событие сигнала на каждой итерации
      // с задержкой 300 миллисекунд,
      // если кнопка  повторения сигнала не нажата. Если кнопка повторения сигнала нажата,
      // задержка равняется 1000 миллисекундам.
      //
      public void Start()
      {
         for (;;) 
         {
            nrings++;     
            if (stop)
            {
               break;
            }
            else if (snoozePressed)
            {
               System.Threading.Thread.Sleep(1000);
               {
                  AlarmEventArgs e = new AlarmEventArgs(snoozePressed,
                     nrings);
                  OnAlarm(e);
               }
            }
            else
            {
               System.Threading.Thread.Sleep(300);
               AlarmEventArgs e = new AlarmEventArgs(snoozePressed,
                  nrings);
               OnAlarm(e);
            }          
         }
      }
   }

   // Класс WakeMeUp(разбуди меня) имеет метод AlarmRang(будильник прозвенел),  
// обрабатывающий событие сигнала.
   //
   public class WakeMeUp
   {
      public void AlarmRang(object sender, AlarmEventArgs e)
      {
         Console.WriteLine(e.AlarmText +"\n");

         if (!(e.SnoozePressed))
         {
            if (e.NumRings % 10 == 0)
            {
               Console.WriteLine(" Позволить будильнику звенеть? Введите Y");
               Console.WriteLine(" Нажать кнопку повторения сигнала? Введите N");
               Console.WriteLine(" Остановить сигнал? Введите Q");
               String input = Console.ReadLine();

               if (input.Equals("Y") ||input.Equals("y")) return;

               else if (input.Equals("N") || input.Equals("n"))
               {
                  ((AlarmClock)sender).SnoozePressed = true;
                  return;
               }
               else
               {
                  ((AlarmClock)sender).Stop = true;
                  return;
               }
            }
         }
         else
         {
            Console.WriteLine("Позволить будильнику звенеть? Введите Y");
            Console.WriteLine("Остановить сигнал? Введите Q");
            String input = Console.ReadLine();
            if (input.Equals("Y") || input.Equals("y")) return;
            else
            {
               ((AlarmClock)sender).Stop = true;
               return;
            }
         }
      }
   }
  
   // Управляющий класс, подключающий метод обработки события
   // WakeMeUp к событию сигнала объекта Alarm(будильник) с помощью делегата.
   // В приложении на базе форм управляющим классом является форма
   //
   //
   public class AlarmDriver
   { 
      public static void Main (string[] args)
      { 
         // Создает экземпляр получателя события.
         WakeMeUp w= new WakeMeUp();
                 
         // Создает экземпляр источника события.
         AlarmClock clock = new AlarmClock();

         // Соединяет метод AlarmRang с событием Alarm.
         clock.Alarm += new AlarmEventHandler(w.AlarmRang);

         clock.Start();
      }
   }
}

Данный код eventsample.cs компилируется со ссылкой на System.dll:

C:\..\v2.0.50727> csc.exe   /reference:System.dll  eventsample.cs

Код выполняется:


Так что такое делегат?

Делегат является классом (использующим ключевое слово delegate), способным хранить ссылку на метод. В отличие от других классов, класс делегата имеет сигнатуру и может хранить ссылки на методы, соответствующие его сигнатуре. Делегат эквивалентен безопасному по типу указателю функции или обратному вызову. Хотя делегаты имеют много иных применений, в данном разделе разбирается функционал обработки событий делегатов. Объявления делегата достаточно для определения класса делегата. Объявление задает сигнатуру делегата, а CLR обеспечивает реализацию. Следующий пример показывает объявление делегата события:

public delegate void AlarmEventHandler(object sender, EventArgs e);

Стандартная сигнатура делегата обработчика события определяет метод, не возвращающий значение, первый параметр которого имеет тип Object(объект) и ссылается на экземпляр, возбуждающий событие, а второй параметр которого получен от типа EventArgs и хранит данные события. Делегат в C# похож на указатель функции в C или C++. Использование делегата позволяет помещать ссылку на метод в объект делегата. Затем объект делегата передается коду, вызывающему метод, на который ссылается делегат (если параметры совпадают), без необходимости знать во время компиляции, какой метод будет вызван. В отличие от указателей функций в C или C++, делегаты привязаны к объекту, безопасны по типу и защищены. Как и CLR (общеязыковая среда исполнения) не должна знать, какой язык .NET скомпилирован, CLR знает, что управляемый код порождает метаданные и код на промежуточном языке. Таблицы метаданных проверяются для выполнения безопасной по типу проверки, обеспечивающей передачу нужных данных нужному методу, так как промежуточный код является своевременным (JIT). Короче, объявление делегата определяет тип, инкапсулирующий метод с определенным набором аргументов и тип возвращаемой переменной. Для статических методов объект делегата инкапсулирует метод, который будет вызываться. Для методов экземпляра объект делегата инкапсулирует экземпляр и метод, основанный на методах. Статический метод и метод экземпляра различаются тем, что первый действует на сам тип, а второй действует на экземпляр типа. Если вы имеете объект делегата и подходящий набор аргументов, то можете вызвать делегат с аргументами.

Изучите следующий код:

using System;

// Набор классов для управления книжным магазином:
namespace Bookstore
{
   using System.Collections;

   // Описывает книгу в списке книг:
   public struct Book
   {
      public string Title;        // Название книги.
      public string Author;       // Автор книги.
      public decimal Price;       // Цена книги.
      public bool Paperback;      // Книга в мягкой обложке?
      public Book(string title, string author, decimal price, bool paperBack)
      {
         Title = title;
         Author = author;
         Price = price;
         Paperback = paperBack;
      }
   }

   // Объявляется тип делегата для обработки книги:
   public delegate void ProcessBookDelegate(Book book);

   // Обслуживает базу данных книг.
   public class BookDB
   {
      // Список всех книг в базе данных:
      ArrayList list = new ArrayList();  

      // Добавляет книгу в базу данных:
      public void AddBook(string title, string author, decimal price, bool paperBack)
      {
         list.Add(new Book(title, author, price, paperBack));
      }

      // Вызывает переданный делегат для обработки каждой книги в бумажной обложке:
      public void ProcessPaperbackBooks(ProcessBookDelegate processBook)
      {
         foreach (Book b in list)
         {
            if (b.Paperback)
            // Вызов делегата:
               processBook(b);
         }
      }
   }
}

// Использование классов книжного магазина:
namespace BookTestClient
{
   using Bookstore;

   // Класс для суммирования цен книг и вычисления средней цены на книгу:
   class PriceTotaller
   {
      int countBooks = 0;
      decimal priceBooks = 0.0m;

      internal void AddBookToTotal(Book book)
      {
         countBooks += 1;
         priceBooks += book.Price;
      }

      internal decimal AveragePrice()
      {
         return priceBooks / countBooks;
      }
   }

   // Класс для тестирования базы данных книг:
   class Test
   {
      // Выводит название книги.
      static void PrintTitle(Book b)
      {
         Console.WriteLine("   {0}", b.Title);
      }

      // Здесь начинается выполнение.
      static void Main()
      {
         BookDB bookDB = new BookDB();

         // Инициализирует базу данных несколькими книгами:
         AddBooks(bookDB);     

         // Выводит все названия книг в мягкой обложке:
         Console.WriteLine("Paperback Book Titles:");
         // Создает новый объект делегата, связанный со статическим
         // методом Test.PrintTitle:
         bookDB.ProcessPaperbackBooks(new ProcessBookDelegate(PrintTitle));

         // Получает среднюю цену книги в мягкой обложке с помощью
         // объекта PriceTotaller:
         PriceTotaller totaller = new PriceTotaller();
         // Создает новый объект делегата, связанный с нестатическим
         // методом AddBookToTotal объекта сумматора:
         bookDB.ProcessPaperbackBooks(new ProcessBookDelegate(totaller.AddBookToTotal));
         Console.WriteLine("Average Paperback Book Price: ${0:#.##}",
            totaller.AveragePrice());
      }

      // Инициализирует базу данных книг несколькими тестовыми книгами:
      static void AddBooks(BookDB bookDB)
      {
         bookDB.AddBook("Язык программирования C",
            "Брайан У. Керниган и Дэннис М. Ричи", 19.95m, true);
         bookDB.AddBook("Стандарт Юникод 2.0",
            "Консорциум Юникода", 39.95m, true);
         bookDB.AddBook("Энциклопедия MS-DOS",
            "Ray Duncan", 129.95m, false);
         bookDB.AddBook("Подсказки Догберта для тупых",
            "Скотт Адамс", 12.00m, true);
      }
   }
}

Теперь изучите вывод:

Названия книг в мягкой обложке:   
Язык программирования C
   Стандарт Юникод 2.0
   Подсказки Догберта для тупых
Средняя цена книги в мягкой обложке: $23.97

Был создан экземпляр делегата, так как раз делегат был объявлен, то надо создать объект делегата и связать его с конкретным методом. Как и все прочие объекты, новый объект делегата создается с помощью выражения new (новый). В этом заключается принципиальное различие между объектно-ориентированным и процедурным языком C: в C, при объявлении переменной компилятор оповещается о типе данных, который будет служить в качестве переменной, имеющей присвоенное значение. Затем компилятор выделяет память, требуемую для данного конкретного типа данных. Однако при создании делегата выражению new передается особый аргумент, записанный как вызов метода, но без аргументов для метода, как показано в следующем операторе:

bookDB.ProcessPaperbackBooks(new ProcessBookDelegate(PrintTitle));

Этот оператор создает новый объект делегата, связанный со статическим методом Test.PrintTitle. Следующий оператор:

bookDB.ProcessPaperbackBooks(new ProcessBookDelegate(totaller.AddBookToTotal));

создает новый объект делегата, связанный с нестатическим методом AddBookToTotal объекта totaller. В обоих случаях новый объект делегата немедленно передается методу ProcessPaperbackBooks. После создания делегата связанный с ним метод никогда не меняется — объекты делегатов неизменяемые.

Реагирование на событие

Скачиваемый файл SystemTimer является приложением Windows Forms, содержащим управляющий элемент «индикатор выполнения». Идея – управлять управляющим элементом «индикатор выполнения» путем реагирования на события таймера. Объекты таймера могут применяться для генерации событий через заданное число миллисекунд. Вы просто создаете проект Windows Forms, перетаскиваете управляющий элемент «индикатор выполнения» на плоскость формы, а затем объявляете объект Timer. Загрузите код в папку по имени TimerEvents в папке Projects вашей версии Visual Studio. Не пытайтесь запустить файл решения из Zip-архива. Извлеките файлы в указанную подпапку папки Projects и дважды щелкните по файлу решения, чтобы увидеть, как индикатор выполнения реагируют после истечения определенного числа миллисекунд.

Ниже приведен пример кода из книги Джеффри Ричтера “CLR через C#”, поясняющий сущность делегатов:

using System;
using System.Windows.Forms;
using System.IO;

// Объявлен тип делегата; экземпляры ссылаются на метод,
// принимающий параметр Int32 и возвращающий void (пустой тип).
internal delegate void Feedback(Int32 value);

public sealed class Program {
   public static void Main() {
      StaticDelegateDemo();
      InstanceDelegateDemo();
      ChainDelegateDemo1(new Program());
      ChainDelegateDemo2(new Program());
   }

   private static void StaticDelegateDemo() {
      Console.WriteLine("----- Пример статического делегата -----");
      Counter(1, 3, null);
      Counter(1, 3, new Feedback(Program.FeedbackToConsole));
      Counter(1, 3, new Feedback(FeedbackToMsgBox)); // "Программа." необязателен
      Console.WriteLine();
   }

   private static void InstanceDelegateDemo() {
      Console.WriteLine("----- Пример делегата экземпляра -----");
      Program p = new Program();
      Counter(1, 3, new Feedback(p.FeedbackToFile));

      Console.WriteLine();
   }

   private static void ChainDelegateDemo1(Program p) {
      Console.WriteLine("----- Пример делегата-цепи 1 -----");
      Feedback fb1 = new Feedback(FeedbackToConsole);
      Feedback fb2 = new Feedback(FeedbackToMsgBox);
      Feedback fb3 = new Feedback(p.FeedbackToFile);

      Feedback fbChain = null;
      fbChain = (Feedback) Delegate.Combine(fbChain, fb1);
      fbChain = (Feedback) Delegate.Combine(fbChain, fb2);
      fbChain = (Feedback) Delegate.Combine(fbChain, fb3);
      Counter(1, 2, fbChain);

      Console.WriteLine();
      fbChain = (Feedback) Delegate.Remove(fbChain,
                           new Feedback(FeedbackToMsgBox));
      Counter(1, 2, fbChain);
   }

   private static void ChainDelegateDemo2(Program p) {
      Console.WriteLine("----- Пример делегата-цепи 2 -----");
      Feedback fb1 = new Feedback(FeedbackToConsole);
      Feedback fb2 = new Feedback(FeedbackToMsgBox);
      Feedback fb3 = new Feedback(p.FeedbackToFile);

      Feedback fbChain = null;
      fbChain += fb1;
      fbChain += fb2;
      fbChain += fb3;
      Counter(1, 2, fbChain);

      Console.WriteLine();
      fbChain -= new Feedback(FeedbackToMsgBox);
      Counter(1, 2, fbChain);
   }
  
   private static void Counter(Int32 from, Int32 to, Feedback fb) {
      for (Int32 val = from; val <= to; val++) {
         // Если какие-либо обратные вызовы заданы, вызывает их
         if (fb != null)
            fb(val);
      }
   }

   private static void FeedbackToConsole(Int32 value) {
      Console.WriteLine("Item=" + value);
   }

   private static void FeedbackToMsgBox(Int32 value) {
      MessageBox.Show("Item=" + value);
   }

   private void FeedbackToFile(Int32 value) {
      StreamWriter sw = new StreamWriter("Status", true);
      sw.WriteLine("Item=" + value);
      sw.Close();
   }
}

При компиляции и выполнении данного кода заметьте, что командная строка отображает строки, соответствующие кнопкам Windows Forms. Теперь вспомните, что делегат обозначает сигнатуру метода обратного вызова. Код начинается с объявления внутреннего делегата, Feedback (обратная связь). Feedback обозначает метод, принимающий параметр (Int32) и возвращающий void. Теперь класс Program определяет закрытый статический метод по имени Counter (счетчик). Этот метод считает целые числа от аргумента from до аргумента to. Counter также принимает fb, являющийся ссылкой на объект делегата Feedback. CLR требует создавать любой объект путем вызова оператора new. Например, объект Patient(пациент) создавался бы так:

Patient p = new Patient();

Оператор new выполняет ряд скрытых операций, не считая использование переменной (в данном случае p) в качестве ссылки, где хранятся параметры конструктора. Counter проходит в цикле по всем целым числам, и для каждого целого числа, если переменная fb не null (пустая), вызывается метод обратного вызова (заданный переменной fb). Этому методу обратного вызова передается значение обрабатываемого элемента и номер элемента. Метод обратного вызова может быть разработан и реализован для обработки каждого элемента любым нужным образом.