СДЕЛАЙТЕ СВОИ УРОКИ ЕЩЁ ЭФФЕКТИВНЕЕ, А ЖИЗНЬ СВОБОДНЕЕ

Благодаря готовым учебным материалам для работы в классе и дистанционно

Скидки до 50 % на комплекты
только до

Готовые ключевые этапы урока всегда будут у вас под рукой

Организационный момент

Проверка знаний

Объяснение материала

Закрепление изученного

Итоги урока

События языка C#

Категория: Информатика

Нажмите, чтобы узнать подробности

Просмотр содержимого документа
«События языка C#»

Классы с событиями

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

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

Например, наследником класса "человек" может быть класс "человек_образованный",

обладающий методами: читать и писать, считать и программировать.


Есть еще один механизм, позволяющий объектам вести себя по-разному в одних и тех же обстоятельствах. Это механизм событий, рассмотрением которого мы сейчас и займемся. Класс, помимо свойств и методов, может иметь события. Содержательно, событием является некоторое специальное состояние, в котором может оказаться объект класса. Так, для объектов класса "человек" событием может быть рождение или смерть, свадьба или развод. О событиях в мире программных объектов чаще всего говорят в связи с интерфейсными объектами, у которых события возникают по причине действий пользователя. Так, командная кнопка может быть нажата - событие Click, документ может быть закрыт - событие Close, в список может быть добавлен новый элемент - событие Changed.


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


  • объявить событие в классе;

  • зажечь в нужный момент событие, передав обработчику необходимые для обработки аргументы. (Под зажиганием или включением события понимается некоторый механизм, позволяющий объекту уведомить клиентов класса, что у него произошло событие.);

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


Заметьте, что, зажигая событие, класс посылает сообщение получателям события -

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

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

Рис. 21.1. Взаимодействие объектов. Посылка и получение сообщения о событии


Класс sender. Как объявляются события?

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


Содержательную сторону будем пояснять на содержательных примерах. А сейчас рассмотрим технический вопрос: как объявляются события средствами языка С#? Прежде всего, уточним, что такое событие с программистской точки зрения. Начнем не с самого события, а с его обработчика. Обработчик события - это обычная процедура с аргументами. Понятно, что сообщение, посылаемое при зажигании события, является аналогом вызова процедуры. Поскольку сигнатура посылаемого сообщения должна соответствовать сигнатуре принимаемого сообщения, то объявление события синтаксически должно задавать сигнатуру процедуры.


Делегаты и события


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


  • Вначале объявляется делегат - функциональный класс, задающий сигнатуру. Как отмечалось при рассмотрении делегатов, объявление делегата может быть помещено в некоторый класс, например, класс Sender. Но, чаще всего, это объявление находится вне класса в пространстве имен. Поскольку одна и та же сигнатура может быть у разных событий, то для них достаточно иметь одного делегата. Для некоторых событий можно использовать стандартных делегатов, встроенных в каркас. Тогда достаточно знать только их имена.

  • Если делегат определен, то в классе Sender, создающем события, достаточно объявить событие как экземпляр соответствующего делегата. Это делается точно так же, как и при объявлении функциональных экземпляров делегата. Исключением является добавление служебного слова event. Формальный синтаксис объявления таков:


[атрибуты] [модификаторы]event [тип, заданный делегатом] [имя события]


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


namespace Events

{

public delegate void FireEventHandler(object Sender, int time, int build);

public class TownWithEvents

{

public event FireEventHandler FireEvent;

...

}//TownWithEvents

...

}//namespace Events


Здесь делегат FireEventHandler описывает класс событий, сигнатура которых содержит три аргумента. Событие FireEvent в классе TownWithEvents является экземпляром класса, заданного делегатом.


Как зажигаются события


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

Приведу пример:


protected virtual void OnFire(int time, int build)

{

if (FireEvent!=null) FireEvent(this,time, build);

}


Хочу обратить внимание: те, кто принимает сообщение о событии, должны заранее присоединить обработчики событий к объекту FireEvent, задающему событие.

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


Заметьте также, что процедура On объявляется, как правило, с модификаторами protected virtual. Это позволяет потомкам класса переопределить ее, когда, например, изменяется набор аргументов события.


Последний шаг, который необходимо выполнить в классе Sender - это в нужных методах класса вызвать процедуру On. Естественно, что перед вызовом нужно определить значения входных аргументов события. После вызова может быть выполнен анализ выходных аргументов, определенных обработчиками события. Чуть позже рассмотрим более полные примеры, где появится вызов процедуры On.


Классы receiver. Как обрабатываются события

Объекты класса Sender создают события и уведомляют о них объекты, возможно, разных классов, названных нами классами Receiver, или клиентами. Давайте разберемся, как должны быть устроены классы Receiver, чтобы вся эта схема заработала.


Понятно, что класс receiver должен:


  • иметь обработчик события - процедуру, согласованную по сигнатуре с функциональным типом делегата, который задает событие;

  • иметь ссылку на объект, создающий событие, чтобы получить доступ к этому событию - event-объекту;

  • уметь присоединить обработчик события к event-объекту. Это можно реализовать по-разному, но технологично это делать непосредственно в конструкторе класса, так

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


public class FireMen

{

private TownWithEvents MyNativeTown; public FireMen(TownWithEvents TWE)

{

this.MyNativeTown=TWE; MyNativeTown.FireEvent += new

FireEventHandler(FireHandler);

}

private void FireHandler(object Sender, int time, int build)

{

Console.WriteLine("Fire at day {0}, in build {1}!", time, build);

}

public void GoOut()

{

MyNativeTown.FireEvent -= new FireEventHandler(FireHandler);

}

}//FireMan


В классе Fireman есть ссылка на объект класса TownWithEvents, создающий события. Сам объект передается в конструкторе класса. Здесь же происходит присоединение обработчика события к event-объекту. Обработчик события FireHandler выводит сообщение на консоль.


Классы с событиями, допустимые в каркасе .Net Framework

Если создавать повторно используемые компоненты с событиями, работающие не только в проекте C#, то необходимо удовлетворять некоторым ограничениям. Эти требования предъявляются к делегату; они носят, скорее, синтаксический характер, не ограничивая существа дела.


Перечислю эти ограничения:


    • делегат, задающий тип события, должен иметь фиксированную сигнатуру из двух аргументов: delegate (object sender, args);

    • первый аргумент задает объект sender, создающий сообщение. Второй аргумент args задает остальные аргументы - входные и выходные, - передаваемые обработчику. Тип этого аргумента должен задаваться классом, производным от встроенного в .Net Framework класса EventArgs. Если обработчику никаких дополнительных аргументов не передается, то следует просто указать класс EventArgs, передавая null в качестве фактического аргумента при включении события;

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


Пример "Списки с событиями"

В этом примере строится класс ListWithChangedEvent, являющийся потомком встроенного класса ArrayList, который позволяет работать со списками. В класс добавляется событие Changed, сигнализирующее обо всех изменениях элементов списка. Строятся два класса - Receiver1 и Receiver2, получающие сообщения. В примере рассматривается взаимодействие нескольких объектов: два объекта посылают сообщения, три - принимают.

Начнем с объявления делегата:


// Объявление делегата

public delegate void ChangedEventHandler(object sender,

ChangedEventArgs args);


Здесь объявлен делегат ChangedEventHandler, по всем правилам хорошего стиля - его имя и его форма соответствует всем требованиям. Второй аргумент, задающий аргументы события, принадлежит классу ChangedEventArgs, производному от встроенного класса EventArgs. Рассмотрим, как устроен этот производный класс:


public class ChangedEventArgs:EventArgs

{

private object item; private bool permit; public object Item

{

get {return(item);} set { item = value;}

}

public bool Permit

{

get {return(permit);} set { permit = value;}

}

}//class ChangedEventArgs


У класса два закрытых свойства, доступ к которым осуществляется через процедуры- свойства get и set. Конечно, можно было бы в данной ситуации сделать их просто public - общедоступными. Свойство Item задает входной аргумент события, передаваемый обработчику события. Булево свойство Permit задает выходной аргумент события, получающий в обработчике значение True, если обработчик события дает добро на изменение элемента.


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


Правильно ли, что обработчик события, а не сам класс, создающий событие, принимает решение о допуске изменения элемента списка? Все зависит от контекста. В прошлые времена молодые могли объявить о своей помолвке, но требовалось разрешение родителей на брак. Времена изменились - теперь на брак родительского благословения не требуется. Но в программистском мире ситуации, требующие внешнего разрешения, встречаются довольно часто.



Класс sender


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


// Класс, создающий событие. Потомок класса ArrayList. public class ListWithChangedEvent: ArrayList

{

//Свойства класса: событие и его аргументы

//Событие Changed, зажигаемое при всех изменениях

//элементов списка.

public event ChangedEventHandler Changed;

//Аргументы события

private ChangedEventArgs evargs = new ChangedEventArgs();

Первое свойство описывает событие Changed. Оно открыто, что позволяет присоединять к нему обработчиков событий. Второе закрытое свойство определяет аргументы события, передаваемые обработчикам.


Хороший стиль требует задания в классе процедуры On, включающей событие. Так и поступим:


//Методы класса: процедура On и переопределяемые методы.

//Процедура On, включающая событие

protected virtual void OnChanged(ChangedEventArgs args)

{

if (Changed != null) Changed(this, args);

}


Процедура OnChanged полностью соответствует ранее описанному образцу, поэтому не требует дополнительных комментариев.


Наш класс, являясь наследником класса ArrayList, наследует все его методы. Переопределим методы, изменяющие элементы:


    • метод Add, добавляющий новый элемент в конец списка;

    • индексатор this, дающий доступ к элементу списка по индексу;

    • метод Clear, производящий чистку списка.


//Переопределяемые методы, вызывающие событие Changed

//Добавление нового элемента

//при получении разрешения у обработчиков события

public override int Add(object value)

{

int i=0;

evargs.Item = value; OnChanged(evargs); if (evargs.Permit)

i = base.Add(value); else

Console.WriteLine("Добавление элемента запрещено." + "Значение = {0}", value);

return i;

}

public override void Clear()

{

evargs.Item=0; OnChanged(evargs); base.Clear();

}

public override object this[int index]

{

set

{

evargs.Item = value; OnChanged(evargs); if (evargs.Permit)

base[index] = value; else

Console.WriteLine("Замена элемента запрещена." + " Значение = {0}", value);

}

get{return(base[index]);}

}


Обратите внимание на схему включения события, например, в процедуре Add. Вначале задаются входные аргументы, в данном случае Item. Затем вызывается процедура

включения OnChanged. При зажигании выполнение процедуры Add прерывается. Запускаются обработчики, присоединенные к событию. Процедура Add продолжит работу только после окончания их работы. Анализ выходной переменной Permit позволяет установить, получено ли разрешение на изменение значения; при истинности значения этой переменной вызывается родительский метод Add, осуществляющий изменение значения. Это достаточно типичная схема работы с событиями.


Классы receiver


Мы построим два класса, объекты которых способны получать и обрабатывать событие Changed. Получать они будут одно и то же сообщение, а обрабатывать его будут по- разному. В нашей модельной задаче различие обработчиков сведется к выдаче разных сообщений. Поэтому достаточно разобраться с устройством одного класса, названного EventReceiver1. Вот его код:


class EventReceiver1

{

private ListWithChangedEvent List;

public EventReceiver1(ListWithChangedEvent list)

{

List = list;

// Присоединяет обработчик к событию. OnConnect();

}

//Обработчик события - выдает сообщение.

//Разрешает добавление элементов, меньших 10. private void ListChanged(object sender,

ChangedEventArgs args)

{

Console.WriteLine("EventReceiver1: Сообщаю об изменениях:" + "Item ={0}", args.Item);

args.Permit = ((int)args.Item

}

public void OnConnect()

{

//Присоединяет обработчик к событию

List.Changed += new ChangedEventHandler(ListChanged);

}

public void OffConnect()

{

//Отсоединяет обработчик от события и удаляет список List.Changed -= new ChangedEventHandler(ListChanged); List = null;

}

}//class EventReceiver1


Дам краткие комментарии.


  • Среди закрытых свойств класса есть ссылка List на объект, создающий события.

  • Конструктору класса передается фактический объект, который и будет присоединен к List. В конструкторе же происходит присоединение обработчика события к событию. Для этого, как положено, используется созданный в классе метод OnConnect.

  • Класс содержит метод OffConnect, позволяющий при необходимости отключить обработчик от события.

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


Класс Receiver2 устроен аналогично. Приведу его текст уже без всяких комментариев:

class Receiver2

{

private ListWithChangedEvent List;

public Receiver2(ListWithChangedEvent list)

{

List = list;

// Присоединяет обработчик к событию. OnConnect();

}

// Обработчик события - выдает сообщение.

//Разрешает добавление элементов, меньших 20. private void ListChanged(object sender,

ChangedEventArgs args)

{

Console.WriteLine("Receiver2: Сообщаю об изменениях:"

+ " Объект класса {0} : " + "Item ={1}", sender.GetType(), args.Item);

args.Permit = ((int)args.Item 20);

}

public void OnConnect()

{

//Присоединяет обработчик к событию

List.Changed += new ChangedEventHandler(ListChanged);

//Заметьте, допустимо только присоединение (+=),

//но не замена (=)

//List.Changed = new ChangedEventHandler(ListChanged);

}

public void OffConnect()

{

//Отсоединяет обработчик от события и удаляет список List.Changed -= new ChangedEventHandler(ListChanged); List = null;

}

}//class Receiver2


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


public void TestChangeList()

{

//Создаются два объекта, вырабатывающие события ListWithChangedEvent list = new ListWithChangedEvent(); ListWithChangedEvent list1 = new ListWithChangedEvent();

//Создаются три объекта двух классов EventReceiver1 и

//Receiver2, способные обрабатывать события класса

//ListWithChangedEvent

EventReceiver1 Receiver1 = new EventReceiver1(list); Receiver2 Receiver21 = new Receiver2 (list); Receiver2 Receiver22 = new Receiver2(list1);

Random rnd = new Random();

//Работа с объектами, приводящая к появлению событий

list.Add(rnd.Next(20)); list.Add(rnd.Next(20)); list[1] =17;

int val = (int)list[0] + (int)list[1];list.Add(val); list.Clear();

list1.Add(10); list1[0] = 25; list1.Clear();

//Отсоединение обработчика событий Receiver1.OffConnect(); list.Add(21); list.Clear();

}


В заключение взгляните на результаты работы этой процедуры.

Рис. 21.2. События в мире объектов


Две проблемы с обработчиками событий

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


Игнорирование коллег


Задумывались ли вы, какую роль играет ключевое слово event, появляющееся при объявлении события? Событие, объявленное в классе, представляет экземпляр делегата. В предыдущей лекции, когда речь шла о делегатах, их экземпляры объявлялись без всяких дополнительных ключевых слов.


Слово "event" играет важную роль, позволяя решить проблему, названную нами "игнорированием коллег". В чем ее суть? В том, что некоторые из классов Receiver могут вести себя некорректно по отношению к своим коллегам, занимающимся обработкой того же события. При присоединении обработчика события в классе Receiver можно попытаться вместо присоединения обработчика выполнить операцию присваивания, игнорируя, тем самым, уже присоединенный список обработчиков. Взгляните еще раз на процедуру OnConnect класса Receiver2; там демонстрируется такая попытка в закомментированном операторе. Аналогично, в процедуре OffConnect вместо отсоединения (операции -) можно попытаться присвоить событию значение null, отсоединяя тем самым всех других обработчиков.


С этим как-то нужно бороться. Ключевое слово "event" дает указание компилятору создать для события закрытое поле, доступ к которому можно получить только через два автоматически создаваемых для события метода: Add, выполняющий операцию присоединения "+=", и Remove, выполняющий обратную операцию отсоединения "-=".

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


Переопределение значений аргументов события


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

Приведенный выше пример "Работа со списками" демонстрирует не самый лучший способ определения аргументов, провоцирующий классы Receiver на некорректное обращение с аргументами. Напомню, в классе ChangedEventArgs, определяющем аргументы события, оба свойства item и permit являются закрытыми. Но определены процедуры - свойства Item и Permit, реализующие полный доступ к свойствам, поскольку определены обе процедуры get и set. Это несколько облегчило задачу, поскольку позволило изменять значение входного аргумента item перед зажиганием события для передачи его обработчику. Но входной аргумент оказался не защищенным, и обработчик события может не только использовать это значение для анализа, но и изменить его в качестве побочного эффекта своей работы. В этом случае другой обработчик будет работать уже с некорректным значением. Что еще хуже - это измененное значение может использовать и класс, в процессе своей дальнейшей работы. Поэтому входные аргументы события должны быть закрытыми для обработчиков событий. Это нетрудно сделать, и я приведу необходимые уточнения.


    • В классе ChangedEventArgs следует изменить процедуру-свойство Item, удалив процедуру Set, разрешающую изменение свойства. В качестве компенсации в класс следует добавить конструктор с аргументом, что позволит в классе, создающем событие, создавать объект класса ChangedEventArgs с нужным значением свойства item. Приведу соответствующий код:

      • public object Item

      • {

      • get {return(item);}

      • //set { item = value;}

      • }

      • public ChangedEventArgs(object item)

      • {

      • this.item = item;

}


    • В методы класса ListWithChangedEvent, зажигающие события, нужно ввести изменения. Теперь перед каждым вызовом нужно создавать новый объект, задающий аргументы. Вот измененный код:

      • public override int Add(object value)

      • {

      • int i=0;

      • ChangedEventArgs evargs = new ChangedEventArgs(value);

      • //evargs.Item = value;

      • OnChanged(evargs);

      • if (evargs.Permit) i = base.Add(value);

      • else

      • Console.WriteLine("Добавление элемента запрещено." +

      • "Значение = {0}", value);

      • return i;

      • }

      • public override void Clear()

      • {

      • ChangedEventArgs evargs = new ChangedEventArgs(0);

      • //evargs.Item=0;

      • OnChanged(evargs);

      • base.Clear();

      • }

      • public override object this[int index]

      • {

      • set

      • {

      • ChangedEventArgs evargs = new ChangedEventArgs(value);

      • //evargs.Item = value;

      • OnChanged(evargs);

  • if (evargs.Permit)

  • base[index] = value;

  • else

  • Console.WriteLine("Замена элемента запрещена." +

  • " Значение = {0}", value);

  • }

  • get {return(base[index]);}

}


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


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


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


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


Классы с большим числом событий

Как было сказано, каждое событие класса представляется полем этого класса. Если у класса много объявленных событий, а реально возникает лишь малая часть из них, то предпочтительнее динамический подход, когда память отводится только фактически возникшим событиям. Это несколько замедляет время выполнения, но экономит память. Решение зависит от того, что в данном контексте важнее - память или время. Для реализации динамического подхода в языке предусмотрена возможность задания пользовательских методов Add и Remove в момент объявления события. Это и есть другая форма объявления события, упоминавшаяся ранее. Вот ее примерный синтаксис:


public event события

{

add {...}

remove {...}

}


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


Давайте построим небольшой пример, демонстрирующий такой способ объявления и работы с событиями. Вначале построим класс с несколькими событиями:


class ManyEvents

{

//хэш таблица для хранения делегатов Hashtable DStore = new Hashtable(); public event EventHandler Ev1

{

add

{

DStore["Ev1"]= (EventHandler)DStore["Ev1"]+ value;

}

remove

{

DStore["Ev1"]= (EventHandler)DStore["Ev1"]- value;

}

}

public event EventHandler Ev2

{

add

{

DStore["Ev2"]= (EventHandler)DStore["Ev2"]+ value;

}

remove

{

DStore["Ev2"]= (EventHandler)DStore["Ev2"]- value;

}

}

public event EventHandler Ev3

{

add

{

DStore["Ev3"]= (EventHandler)DStore["Ev3"]+ value;

}

remove

{

DStore["Ev3"]= (EventHandler)DStore["Ev3"]- value;

}

}

public event EventHandler Ev4

{

add

{

DStore["Ev4"]= (EventHandler)DStore["Ev4"]+ value;

}

remove

{

DStore["Ev4"]= (EventHandler)DStore["Ev4"]- value;

}

}

public void SimulateEvs()

{

EventHandler ev = (EventHandler) DStore["Ev1"]; if(ev != null) ev(this, null);

ev = (EventHandler) DStore["Ev3"]; if(ev != null) ev(this, null);

}

}//class ManyEvents


В нашем классе созданы четыре события и хэш-таблица DStore для их хранения. Все события принадлежат встроенному классу EventHandler. Когда к событию будет присоединяться обработчик, автоматически будет вызван метод add, который динамически создаст элемент хэш-таблиц. Ключом элемента является, в данном случае, строка с именем события. При отсоединении обработчика будет исполняться метод remove, выполняющий аналогичную операцию над соответствующим элементом хэш-таблицы. В классе определен также метод SimulateEvs, при вызове которого зажигаются два из четырех событий - Ev1 и Ev3.


Рассмотрим теперь класс ReceiverEvs, слушающий события. Этот класс построен по описанным ранее правилам. В нем есть ссылка на класс, создающий события; конструктор с параметром, которому передается реальный объект такого класса; четыре обработчика события - по одному на каждое, и метод OnConnect, связывающий обработчиков с событиями. Вот код класса:

class ReceiverEvs

{

private ManyEvents manyEvs;

public ReceiverEvs( ManyEvents manyEvs)

{

this.manyEvs = manyEvs; OnConnect();

}

public void OnConnect()

{

manyEvs.Ev1 += new EventHandler(H1); manyEvs.Ev2 += new EventHandler(H2); manyEvs.Ev3 += new EventHandler(H3); manyEvs.Ev4 += new EventHandler(H4);

}

public void H1(object s, EventArgs e)

{

Console.WriteLine("Событие Ev1");

}

public void H2(object s, EventArgs e)

{

Console.WriteLine("Событие Ev2");

}

public void H3(object s, EventArgs e)

{

Console.WriteLine("Событие Ev3");

}

public void H4(object s, EventArgs e)

{

Console.WriteLine("Событие Ev4");

}

}//class ReceiverEvs


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


public void TestManyEvents()

{

ManyEvents me = new ManyEvents(); ReceiverEvs revs = new ReceiverEvs(me); me.SimulateEvs();

}


Все работает предусмотренным образом.


Проект "Город и его службы"

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


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


public class NewTown

{

//свойства

private int build, BuildingNumber; //дом и число домов в городе

private int day, days; //Текущий день года

//городские службы

private Police policeman ; private Ambulance ambulanceman ; private FireDetect fireman ;

//события в городе

public event FireEventHandler Fire;

//моделирование случайных событий

private Random rnd = new Random();

//вероятность пожара в доме в текущий день: p= m/n private int m = 3, n= 10000;


В нашем городе есть дома; есть время, текущее день за днем; городские службы; событие "пожар", которое, к сожалению, может случайно с заданной вероятностью возникать каждый день в каждом доме. Рассмотрим конструктор объектов нашего класса:


//конструктор класса

public NewTown(int TownSize, int Days)

{

BuildingNumber = rnd.Next(TownSize); days = Days;

policeman = new Police(this); ambulanceman= new Ambulance(this); fireman= new FireDetect(this); policeman.On();

ambulanceman.On(); fireman.On();

}


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


В соответствии с ранее описанной технологией определим метод OnFire, включающий событие:


protected virtual void OnFire(FireEventArgs e)

{

if(Fire != null) Fire(this, e);

}


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


public void LifeOurTown()

{

for(day = 1; day

for(build =1; build BuildingNumber; build++)

{

if( rnd.Next(n) //загорелся дом

{

//аргументы события

FireEventArgs e = new FireEventArgs(build, day, true); OnFire(e);

if(e.Permit)

Console.WriteLine("Пожар потушен!" + " Ситуация нормализована.");

else Console.WriteLine("Пожар продолжается." + " Требуются дополнительные средства!");

}

}

}


Рассмотрим теперь классы Receiver, обрабатывающие событие Fire. Их у нас три, по одному на каждую городскую службу. Все три класса устроены по одному образцу. Напомню, каждый такой разумно устроенный класс, кроме обработчика события, имеет конструктор, инициализирующий ссылку на объект, создающий события, методы подключения и отсоединения обработчика от события. В такой ситуации целесообразно построить вначале абстрактный класс Receiver, в котором будет предусмотрен обработчик события, но не задана его реализация, а затем для каждой службы построить класс- потомок. Начнем с описания родительского класса:


public abstract class Receiver

{

private NewTown town;

public Receiver(NewTown town)

{this.town = town;} public void On()

{

town.Fire += new FireEventHandler(It_is_Fire);

}

public void Off()

{

town.Fire -= new FireEventHandler(It_is_Fire); town = null;

}

public abstract void It_is_Fire(object sender, FireEventArgs e);

}//class Receiver


Для классов потомков абстрактный метод It_is_Fire будет определен. Вот их описания:


public class Police : Receiver

{

public Police (NewTown town): base(town){} public override void It_is_Fire(object sender,

FireEventArgs e)

{

Console.WriteLine("Пожар в доме {0}. День {1}-й."

+ " Милиция ищет виновных!", e.Build,e.Day); e.Permit &= true;

}

}// class Police

public class FireDetect : Receiver

{

public FireDetect (NewTown town): base(town){}

public override void It_is_Fire(object sender, FireEventArgs e)

{

Console.WriteLine("Пожар в доме {0}. День {1}-й."+ " Пожарные тушат пожар!", e.Build,e.Day);

Random rnd = new Random(e.Build); if(rnd.Next(10) 5)

e.Permit &= false; else e.Permit &=true;

}

}// class FireDetect

public class Ambulance : Receiver

{

public Ambulance(NewTown town): base(town){} public override void It_is_Fire(object sender,

FireEventArgs e)

{

Console.WriteLine("Пожар в доме {0}. День {1}-й."+ " Скорая спасает пострадавших!", e.Build,e.Day);

e.Permit &= true;

}

}// class Ambulance


Для каждого потомка задан конструктор, вызывающий базовый метод родителя. Каждый потомок по-своему определяет обработчика события Fire. Обратите внимание на то, как в данном проекте решается проблема с выходным параметром события - Permit. Принята следующая стратегия: возвращаемое значение Permit будет истинно, если все обработчики согласны с этим. Поэтому каждый обработчик использует конъюнкцию выработанного им значения со значением, пришедшим от предыдущего обработчика. В данном примере все зависит от пожарных, которые могут вырабатывать разные решения.


Для полноты картины необходимо показать, как выглядит класс, задающий аргументы события, который, как и положено, является потомком класса EventArgs:


public class FireEventArgs : EventArgs

{

private int build; private int day; private bool permit; public int Build

{

get{ return(build);} ///set{ build = value;}

}

public int Day

{

get{ return(day);} ///set{ day = value;}

}

public bool Permit

{

get{ return(permit);} set{ permit = value;}

}

public FireEventArgs(int build, int day, bool permit)

{

this.build = build; this.day = day; this.permit = permit;

}

}//class FireEventArgs


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


Для завершения проекта нам осталось определить тестирующую процедуру в классе

Testing, создающую объекты и запускающую моделирование жизни города:


public void TestLifeTown()

{

NewTown sometown = new NewTown(100,100); sometown.LifeOurTown();

}


Результаты ее работы зависят от случайностей. Вот как выглядит один из экспериментов:


Рис. 21.3. События в жизни города