ооп теория
.pdf
Рис. 21.1. Взаимодействие объектов. Посылка и получение сообщения о событии
Класс sender. Как объявляются события?
При проектировании класса с событиями, возможно, самое трудное -
содержательная сторона дела. Какими событиями должен обладать класс, в
каких методах и в какой момент зажигать то или иное событие?
Содержательную сторону будем пояснять на содержательных примерах. А
сейчас рассмотрим технический вопрос: как объявляются события средствами языка С#? Прежде всего, уточним, что такое событие с программистской точки зрения. Начнем не с самого события, а с его обработчика. Обработчик события - это обычная процедура с аргументами.
Понятно, что сообщение, посылаемое при зажигании события, является аналогом вызова процедуры. Поскольку сигнатура посылаемого сообщения должна соответствовать сигнатуре принимаемого сообщения, то объявление события синтаксически должно задавать сигнатуру процедуры.
ДЕЛЕГАТЫ И СОБЫТИЯ
Наверное, вы уже заметили, что схема работы с событиями вполне укладывается в механизм, определяемый делегатами. В C# каждое событие
411
определяется делегатом, описывающим сигнатуру сообщения. Объявление события - это двухэтапный процесс:
Вначале объявляется делегат - функциональный класс, задающий сигнатуру. Как отмечалось при рассмотрении делегатов, объявление делегата может быть помещено в некоторый класс, например, класс
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
является экземпляром класса, заданного делегатом.
412
Как зажигаются события
Причины возникновения события могут быть разными. Поэтому вполне вероятно, что одно и то же событие будет зажигаться в разных методах класса в тот момент, когда возникнет одна из причин появления события.
Поскольку действия по включению могут повторяться, полезно в состав методов класса добавить защищенную процедуру, включающую событие.
Даже если событие зажигается только в одной точке, написание такой процедуры считается признаком хорошего стиля. Этой процедуре обычно дается имя, начинающееся со слова On, после которого следует имя события.
Будем называть такую процедуру On-процедурой. Она проста и состоит из вызова объявленного события, включенного в тест, который проверяет перед вызовом, а есть ли хоть один обработчик события, способный принять соответствующее сообщение. Если таковых нет, то нечего включать событие.
Приведу пример:
protected virtual void OnFire(int time, int build)
{
if (FireEvent!=null) FireEvent(this,time, build);
}
Хочу обратить внимание: те, кто принимает сообщение о событии, должны заранее присоединить обработчики событий к объекту FireEvent, задающему событие. Присоединение обработчиков должно предшествовать зажиганию события. При таком нормальном ходе вещей, найдется хотя бы один слушатель сообщения о событии - следовательно, FireEvent не будет равно null.
Заметьте также, что процедура On объявляется, как правило, с
модификаторами protected virtual. Это позволяет потомкам класса переопределить ее, когда, например, изменяется набор аргументов события.
413
Последний шаг, который необходимо выполнить в классе 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);
}
414
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, например,
415
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
416
У класса два закрытых свойства, доступ к которым осуществляется через процедуры-свойства get и set. Конечно, можно было бы в данной ситуации сделать их просто public - общедоступными. Свойство Item задает входной аргумент события, передаваемый обработчику события. Булево свойство
Permit задает выходной аргумент события, получающий в обработчике значение True, если обработчик события дает добро на изменение элемента.
В модели, которую мы рассматриваем, предполагается, что обработчик события, получив уведомление об изменении элемента, анализирует ситуацию и может разрешить или не разрешить изменение, например, если значение элемента больше некоторого предельного значения.
Правильно ли, что обработчик события, а не сам класс, создающий событие,
принимает решение о допуске изменения элемента списка? Все зависит от контекста. В прошлые времена молодые могли объявить о своей помолвке,
но требовалось разрешение родителей на брак. Времена изменились - теперь на брак родительского благословения не требуется. Но в программистском мире ситуации, требующие внешнего разрешения, встречаются довольно часто.
Класс sender
Рассмотрим теперь, как устроен в нашем примере класс, создающий события.
Начнем со свойств класса:
// Класс, создающий событие. Потомок класса ArrayList. public class ListWithChangedEvent: ArrayList
{
//Свойства класса: событие и его аргументы //Событие Changed, зажигаемое при всех изменениях //элементов списка.
public event ChangedEventHandler Changed; //Аргументы события
private ChangedEventArgs evargs = new ChangedEventArgs();
417
Первое свойство описывает событие 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);
418
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)
{
419
Console.WriteLine("EventReceiver1: Сообщаю об изменениях:" + "Item ={0}", args.Item);
args.Permit = ((int)args.Item < 10);
}
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();
420
