События

Во многих случаях использования делегатов в качестве механизма обратного вызова может понадобиться просто известить кого-то о наступлении некоторого события вроде щелчка на кнопке в пользовательском интерфейсе. Предположим, что проектируется приложение медиа-проигрывателя. Где-то в пользовательском интерфейсе имеется кнопка “Play” (Воспроизведение).

В хорошо спроектированной системе пользовательский интерфейс отделен от логики управления с помощью четко определенной абстракции, обычно реализуемой через шаблон Bridge (Мост). Абстракция облегчает последующее изменение пользовательского интерфейса или, что еще лучше, поскольку этот интерфейс зависит от платформы — облегчает перенос приложения на другую платформу.

Например, шаблон Bridge хорошо работает в ситуациях, когда требуется отделить логику управления от пользовательского интерфейса.

Смысл шаблона Bridge заключается в отделении абстракции от реализации, чтобы то и другое можно было менять независимо.

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

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

С другой стороны, делегаты представляют собой блестящий механизм для использования в сценариях подобного рода.

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

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

Для системы это не имеет значения, и в любой момент можно изменить любой компонент взаимодействия, не затрагивая остальное. Обе стороны выполняют требования согласованного контракта, который в данном случае включает специально сформированный делегат и средства его регистрации в генерирующей события сущности.

Этот шаблон использования, также известный под названием “издатель/подписчик” (publish/subscribe), настолько распространен, даже за пределами мира разработки пользовательских интерфейсов, что проектировщики исполняющей системы .NET позаботились об определении формализованного встроенного механизма событий.

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
using System; // Аргументы, переданные от пользовательского интерфейса
// при возникновении события включения воспроизведения,
public class PlayEventArgs : EventArgs {
public PlayEventArgs ( string filename )  {
this.filename = filename;
}
private string filename;
public string Filename {
get {
return filename; }
}
public class PlayerUI {
// Определить событие для уведомления о воспроизведении,
public event EventHandler<playEventArgs> PlayEvent;
public void UserPressedPlay()  {
OnPlay();
}
protected virtual void OnPlay()  {
// Инициировать событие.
EventHandler<playEventArgs> localHandler
= PlayEvent;
if( localHandler != null )  {
localHandler ( this, new PlayEventArgs("somefile.wav")  );
}
public class CorePlayer {
public CorePlayer()  {
ui = new PlayerUI () ; // Регистрация обработчика события,
ui.PlayEvent += this.PlaySomething;
}
private void PlaySomething ( object source, PlayEventArgs args )  {
// Воспроизведение файла.
}
private PlayerUI ui;
}
public class EntryPoint {
static void Main()  {
CorePlayer player = new CorePlayer ();
}

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

Этот контракт инкапсулирован внутри класса PlayEventArgs, который унаследован от System.EventArgs (как описано ниже). накладывают определенные правила на использование делегатов. Делегат должен что-нибудь возвращать и должен принимать два аргумента, как показано в методе PlaySomething из предыдущего примера.

Первый аргумент — это ссылка на объект, представляющий сторону, которая генерирует сообщение, а второй аргумент — тип, унаследованный от System.EventArgs. В этом производном классе определяются все специфичные для события аргументы.

В .NET 1.1 приходилось явно определять тип делегата, стоящего за событием. Начиная с .NET 2.0, можно использовать новый обобщенный делегат EventHandler, защищающий от этой рутинной работы.

Обратите внимание на способ определения события внутри класса PlayerUI с применением ключевого слова event. За этим ключевым словом сначала следует определенный делегат события, а за ним — имя события, в данном случае PlayEvent. Также отметьте, что член-событие объявлено с использованием обобщенного делегата EventHandler.

При регистрации обработчиков с применением операции += в качестве сокращения, можно предоставлять только имя метода для вызова, а компилятор создаст экземпляр EventHandler, используя группу методов для делегирования правил присваивания.

После операции += можно дополнительно указать выражение, создающее новый экземпляр EventHandler, как это делалось при создании экземпляров делегатов, но если компилятор предлагает показанное сокращение, то зачем применять громоздкий синтаксис, затрудняющий чтение кода?

Идентификатор PlayEvent означает две совершенно разные вещи, в зависимости от того, с какой точки зрения его рассматривать. С точки зрения генератора события — в данном случае, PlayerUI — событие PlayEvent используется в точности как делегат.

Такое его применение можно видеть внутри метода OnPlay. Обычно метод, названный OnPlay, вызывается в ответ на щелчок на кнопке пользовательского интерфейса. Он уведомляет всех зарегистрированных слушателей о вызове через событие (делегат) PlayEvent.

При генерации событий существует популярный подход — инициировать их внутри метода protected virtual по имени Оn<событие>, где <событие> заменяется именем события, в данном случае — OnPlay. Подобным образом производные классы могут легко модифицировать действия, предпринимаемые, когда должно быть инициировано событие.

В С# необходимо проверить событие на равенство null, прежде чем вызывать его, иначе будет сгенерировано исключение NullReferenceException. Перед проверкой на null метод OnPlay создает локальную копию события. Это позволяет избежать условия состязаний, когда событие устанавливается в null из другого потока после выполнения проверки на null и перед генерацией события.

Как видно в конструкторе CorePlayer, со стороны потребителя события идентификатор PlayEvent используется совершенно иначе.

Такова базовая структура событий. Как упоминалось ранее, события .NET — это сокращения для создания делегатов и контрактов, с которыми нужно регистрировать эти делегаты. В доказательство этого можно просмотреть код IL, полученный в результате компиляции предыдущего примера. “За кулисами” компилятор генерирует два метода addOnPlay и removeOnPlay, которые вызываются, когда используются перегруженные операции += и -=.

Эти методы управляют добавлением и удалением делегатов в цепочке делегатов событий. В действительности компилятор С# не позволяет вызывать эти методы явно, так что должны использоваться перегруженные операции.

Может возникнуть вопрос, а есть ли какой-нибудь способ контролировать тело этих функций-членов, как это делается со свойствами? Ответ — да, и используемый для этого синтаксис подобен синтаксису свойств.

Ниже показан измененный код класса PlayerUI, в котором демонстрируется явный способ обработки добавления и удаления операций событий.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class PlayerUI {
// Определить событие для уведомления о воспроизведении,
private EventHandler<playEventArgs> playEvent;
public event EventHandler<playEventArgs> PlayEvent {
add {
playEvent = (EventHandler<playEventArgs>)
Delegate.Combine( playEvent, value );
}
remove {
playEvent = (EventHandler<playEventArgs>)
Delegate.Remove( playEvent, value );
}
public void UserPressedPlay()  {
OnPlay();
}
protected virtual void OnPlay()  {
// Инициировать событие.
EventHandler<playEventArgs> localHandler = playEvent;
if( localHandler != null )  {
localHandler ( this,
new PlayEventArgs("somefile.wav")  );
}

Внутри разделов add и remove объявления события ссылка на добавляемый или удаляемый делегат осуществляется по ключевому слову value, что идентично тому, как работает метод set свойства. Данный пример использует Delegate.Combine и Delegate. Remove для управления внутренней цепочкой делегатов по имени playEvent.

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

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

А теперь заключительный комментарий относительно шаблонов проектирования. На основе сказанного можно сделать вывод, что события идеальны для реализации шаблона проектирования “издатель/подписчик”, когда множество слушателей регистрируются для получения уведомления о событии (о его публикации).

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

Вы можете следить за любыми ответами на эту запись через RSS 2.0 ленту. Вы можете оставить ответ, или trackback с вашего собственного сайта.

Оставьте отзыв

XHTML: Вы можете использовать следующие теги: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

 
Rambler's Top100