Основы объектного программирования в классах на C# 3.0

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

Разбить на страницы
Показывать лекцию целиком

Проект к данной лекции Вы можете скачать здесь.

Специфика поведения объектов

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

Хотя реализация методов у всех объектов одна, это не значит, что результат выполнения одного и того же метода, вызванного разыми объектами, будет один и тот же, поскольку выполнение метода зависит от свойств. Так что методы " Пробежать километр " и " Решить задачу " будут давать разные результаты для разных объектов класса " Человек ".

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

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

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

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

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

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

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

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

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

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

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

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

  • Вначале объявляется делегат - функциональный класс, задающий сигнатуру. Как отмечалось при рассмотрении делегатов, объявление делегата может быть помещено в некоторый класс, например, класс sender. Но чаще всего это объявление находится вне класса в пространстве имен. Поскольку одна и та же сигнатура может быть у разных событий, для них достаточно иметь одного делегата. Для некоторых событий можно использовать стандартные делегаты, встроенные в каркас. Тогда достаточно знать только их имена.
  • Если делегат определен, то в классе sender, создающем события, достаточно объявить событие как экземпляр соответствующего делегата. Это делается точно так же, как и при объявлении функциональных экземпляров делегата. Исключением является добавление служебного слова event. Формальный синтаксис объявления таков:
    [атрибуты] [модификаторы]event [тип, заданный делегатом] [имя события]
  • Есть еще одна форма объявления, но о ней чуть позже. Чаще всего атрибуты не задаются, а работа происходит с модификатором доступа - public. Приведу пример объявления делегата и события, представляющего экземпляр этого делегата:

    namespace Events
    {   
      public delegate void FireEvent(int day, int building);
       public class TownWithEvents
       {
          public event FireEvent fireEvent;
          …   
       }//TownWithEvents
          …
    }//namespace Events

    В этом примере в классе TownWithEvents введено событие. Делегат FireEvent описывает класс событий, сигнатура которых содержит два аргумента, характеризующих событие. Объект fireEvent в классе TownWithEvents является экземпляром класса, заданного делегатом. Поскольку он снабжен модификатором event, он является событием со всеми вытекающими отсюда последствиями. Модификатор доступа public делает событие доступным для клиентов класса, обрабатывающего это событие.

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

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

    /// <summary>
    /// "Зажигание" события Fire (пожар)
    /// </summary>
    /// <param name="time">время обнаружения пожара</param>
    /// <param name="build">
      /// здание, в котором произошел пожар </param>
    void OnFire(int day, int buildings)
    {
       if (fireEvent!=null)
          fireEvent(day, buildings);
    }

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

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

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

    string townName;
    int buildings;
    int days;
    double fireProbability;
    Random random=new Random();

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

    Приведу конструктор класса и метод-свойство, обеспечивающее доступ к одному из закрытых полей класса:

    /// <summary>
      /// Конструктор, инициализирующий поля класса
      /// </summary>
      /// <param name="name">название города</param>
      /// <param name="buildings">число домов</param>
      /// <param name="days">число дней жизни города</param>
      /// <param name="fireProbability">
      /// вероятность пожара в доме в текущий день</param>
          public TownWithEvents(string name, int buildings,
          int days, double fireProbability)
          {
          townName = name;
          this.buildings = buildings;
          this.days = days;
          this.fireProbability = fireProbability;
          }
    /// <summary>
      /// Доступ к полю townName
      /// </summary>
      public string TownName
      { get { return townName; } }

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

    /// <summary>
    /// Моделирование жизни города
    /// </summary>
    public void TownLife()
      {
            const string OK =
                "В городе все спокойно! Пожаров не было.";
            bool wasFire = false;         
         for(int day = 1; day < days; day++)
                for(int building = 1; building < buildings; building++)
                    if (random.NextDouble() < fireProbability)
                {                   
                   OnFire(day,building);
                   wasFire = true;
                }         
            if (!wasFire)
                Console.WriteLine(OK);            
      }

    Мы разобрались в том, как создаются события в классе sender. Давайте теперь рассмотрим классы, принимающие событие.

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

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

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

  • иметь обработчик события - процедуру, согласованную по сигнатуре с функциональным типом делегата, который задает событие;
  • иметь ссылку на объект, создающий событие, чтобы получить доступ к событию - event -объекту;
  • уметь присоединить обработчик события к event -объекту. Присоединение можно реализовать по-разному. Это можно делать непосредственно в конструкторе класса, которому передается в качестве аргумента объект, посылающий сообщения. В этом случае уже при создании объект, получающий сообщение, изначально готов принимать и обрабатывать сообщения о событиях. Такое решение хорошо, когда класс настроен на прием сообщений от одного объекта. В других ситуациях удобнее иметь специальный метод, которому передается объект класса sender. В этом методе и происходит присоединение обработчика события к событию того объекта, который передан методу.
  • Рассмотрим пример, демонстрирующий возможное решение проблем:

    /// <summary>
        /// Пожарная служба - класс Reciver
        /// Принимает и обрабатывает событие FireEvent
        /// пожар в городе, клиентом которого является класс.
        /// </summary>
        public class FireMen
        {
            /// <summary>
            /// Город, клиентом которого является класс FireMen
            /// </summary>
            private TownWithEvents MyNativeTown;
            public FireMen(TownWithEvents twe)
            {
                this.MyNativeTown = twe;
                MyNativeTown.fireEvent += new FireEvent(FireHandler);
            }
            /// <summary>
            /// Обработчик события "пожар в городе"
            /// </summary>
            /// <param name="day">день пожара</param>
            /// <param name="buildings">строение</param>
            private void FireHandler(int day, int buildings)
            {
               string message = 
                   "В городе {0} произошел пожар! " +
                   "В день {1}, в доме № {2}" +
                   "  Пожар потушен!";    
               Console.WriteLine(string.Format(message, MyNativeTown.TownName,
                  day, buildings));
            }
            public void GoOut()
            {
                MyNativeTown.fireEvent -= new FireEvent(FireHandler);
            }
        }//FireMan

    В классе Fireman есть ссылка на объект класса TownWithEvents, создающий события. Сам объект передается конструктору класса. В конструкторе метод класса, задающий обработку события, присоединяется к списку вызовов event -объекта. Возможность комбинирования делегатов в полной мере проявляется при работе с событиями. Благодаря этой возможности одно и то же событие будет обрабатываться методами, принадлежащими разным классам receiver.

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

    public void TestTown()
    {
        string townName = "Энск";
        int buildings = 1000;
        int days = 30;
        double fireProbability = 1e-4;
        TownWithEvents town = new 
            TownWithEvents(townName, buildings, days,fireProbability);
        FireMen fireMen = new FireMen(town);
        Console.WriteLine("События в городе " + townName);
        town.TownLife();
    }

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

    (рис 7.2) Моделирование жизни города

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

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

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

  • делегат, задающий тип события, должен иметь фиксированную сигнатуру из двух аргументов: delegate <Имя_делегата> (object sender, <Тип_аргументов> args) ;
  • первый аргумент задает объект sender, создающий сообщение. Второй аргумент args задает остальные аргументы - входные и выходные, - передаваемые обработчику. Тип этого аргумента должен задаваться классом, наследником класса EventArgs из библиотеки классов FCL. Если обработчику никаких дополнительных аргументов не передается, то для второго аргумента следует просто указать класс EventArgs, передавая null в качестве фактического аргумента при включении события;
  • рекомендуемое имя делегата - составное, начинающееся именем события, после которого следует слово EventHandler, например, FireEventHandler. Если никаких дополнительных аргументов обработчику не передается, то тогда можно вообще делегата не объявлять, а пользоваться стандартным делегатом с именем EventHandler.
  • Две проблемы с обработчиками событий

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

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

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

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

    list.Changed += new ChangedEventHandler(ListChanged);
    //list.Changed = new ChangedEventHandler(ListChanged);

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

    list.Changed -= new ChangedEventHandler(ListChanged);
    //list.Changed = null;

    С этим как-то нужно бороться. Ключевое слово " event " разрешает выполнять над событиями только операцию присоединения обработчика событий к списку вызовов " += " и обратную операцию отсоединения " -= ". Никаких других операций над событиями выполнять нельзя. Тем самым, к счастью, решается проблема игнорирования коллег. Ошибки некорректного поведения класса receiver ловятся еще на этапе трансляции. В приведенных примерах некорректные попытки работы закомментированы.

    Изменение аргументов

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

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

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

    Долгое время я считал, что проблема выходных аргументов вообще не может быть корректно решена и представляет "дыру" в языке C#. Об этом я писал в предыдущем издании книги по C#. К счастью, я ошибался и теперь могу привести корректное решение проблемы. Решение проблемы означает, что объект, зажигающий событие, может получить значения выходных аргументов от каждого из обработчиков события в отдельности. Как это сделать?

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

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

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

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

    // Объявление делегата
       public delegate void ChangedEventHandler(object sender,
                     ChangedEventArgs args);

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

    /// <summary>
    /// Дополнительные аргументы события
    /// </summary>
     public class ChangedEventArgs:EventArgs
    {
         string name;    //входной аргумент
         object item;    //входной аргумент
         bool permit;    //выходной аргумент
         public string Name
         { get { return name; } }    //только чтение
           public object Item
         { get { return (item); } }    //только чтение
       public bool Permit
       {
          get {return(permit);}
        set { permit = value; }  //чтение и запись
       }
       public ChangedEventArgs( string name, object item)
       {
             this.name = name; this.item = item; 
         }
       public ChangedEventArgs()
       {}
       }//class ChangedEventArgs

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

    У класса два конструктора: один - без аргументов, второму передаются входные аргументы.

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

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

    Класс sender

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

    /// <summary>
       /// Класс, создающий событие.
        /// Потомок класса ArrayList.
       /// </summary> 
       public class ListWithChangedEvent: ArrayList 
       {      
        string name;    //имя объекта   
          public event ChangedEventHandler Changed;   //событие
        bool permit;  //результат обработки события
          /// <summary> 
             /// Конструктор
           /// </summary>
           /// <param name="name">имя объекта</param>
           public ListWithChangedEvent(string name)
            { this.name = name; }        
            public string Name
            { get { return name; } }

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

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

    /// <summary>
    /// Процедура On, включающая событие
    /// </summary>
    /// <param name="args">аргументы события</param> 
    protected virtual void OnChanged(ChangedEventArgs args) 
    {
       if (Changed != null)
          Changed(this, args);
    }

    Процедура OnChanged соответствует ранее описанному образцу. Если список обработчиков не пуст, то зажигается событие - посылается сообщение всем обработчикам события. Синтаксически конструкция Changed(this, args) - это вызов списка методов, поскольку Changed - объект функционального типа, к которому прикреплен список последовательно работающих методов.

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

    protected virtual void OnChanged(ChangedEventArgs args) 
       {
         int countYes = 0, countNo = 0;
         if (Changed != null)
         {                
            foreach (ChangedEventHandler del in Changed.GetInvocationList())
            {
                 del(this, args);
                 if (args.Permit) countYes++;
                 else countNo++;
            }
            permit = (countYes >= countNo); 
         }
       }

    Метод GetInvocationList позволяет получить список обработчиков события, а цикл foreach - вызывать один обработчик за другим. После того, как обработчик завершится, можно понять, каково значение сформированного выходного аргумента события. В данном примере окончательное решение принимается по большинству голосов. Счетчики countYes и countNo считают голоса "за" и "против". Представленное здесь решение демонстрирует корректный способ работы с событиями, когда обработчикам необходимо передавать входные и выходные аргументы.

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

  • метод Add, добавляющий новый элемент в конец списка;
  • индексатор this, дающий доступ к элементу списка по индексу;
  • метод Clear, производящий чистку списка.
  • // Переопределяемые методы, вызывающие событие Changed
          public override int Add(object value) 
          {
             int index = -1;
             ChangedEventArgs evargs = 
                    new ChangedEventArgs(name, value);
             OnChanged(evargs);
             if (permit) 
                index = base.Add(value);         
             return index;
          }

    Обратите внимание на схему включения события в процедуре Add. Вначале создается объект evargs - аргументы события, который передается методу OnChanged. Этот метод поочередно вызовет обработчики события и сформирует итоговый результат их работы. Анализ переменной permit позволяет установить, получено ли разрешение на изменение значения. При истинности значения этой переменной вызывается родительский метод Add, осуществляющий изменение значения. Аналогично устроены и другие методы, в которых возникает событие Changed.

    public override void Clear() 
    {
       ChangedEventArgs evargs = 
        new ChangedEventArgs(name, 0);
       OnChanged(evargs);
       base.Clear();
    }
    
    public override object this[int index] 
    {
       set 
       {
          ChangedEventArgs evargs = 
                  new ChangedEventArgs(name, value);
          OnChanged(evargs);
              if (permit) 
             base[index] = value;         
       }
       get   {return(base[index]);}
    }

    Это достаточно типичная схема организации класса с событиями.

    Классы receiver

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

    class Receiver1 
       {
          private ListWithChangedEvent list;
            /// <summary>
            /// Конструктор 
            /// Присоединяет обработчик к событию
            /// </summary>
            /// <param name="list">объект, посылающий сообщение</param>
          public Receiver1(ListWithChangedEvent list) 
          {
             this.list = list;
             // Присоединяет обработчик к событию.
                list.Changed += new ChangedEventHandler(ListChanged);
          }         
          public void OffConnect() 
          {
             // Отсоединяет обработчик  
             list.Changed -= new ChangedEventHandler(ListChanged);
          }
       }//class Receiver1

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

    Давайте подробнее рассмотрим устройство обработчика события:

    private void ListChanged(object sender, ChangedEventArgs args) 
      {     
        Console.WriteLine("Сообщение послал {0}, " + 
        "элемент = {1}. " + " Сообщение получил: Receiver1 - ",
        args.Name, args.Item);
        if(args.Permit = (int)args.Item < 10)     
            Console.WriteLine("Изменения разрешаю");
        else Console.WriteLine("Изменения не разрешаю");
          }

    С входными аргументами все просто. Они используются для формирования сообщения и формирования принимаемого решения. Но стоит подробнее разобраться, как в данном конкретном случае формируется выходной аргумент. Свое решение "Разрешить или не разрешить изменение" обработчик принимает в зависимости от величины элемента ( item ). Свое решение обработчик события принимает независимо, без оглядки на методы, совместно с ним обрабатывающими то же событие.

    Класс Receiver2 в отличие от класса Receiver1 позволяет слушать и обрабатывать сообщения нескольких объектов класса sender. С конструктора класса снимается задача связывания события с обработчиком события. Ему не нужно теперь передавать объект класса sender. У класса появляется специальный метод OnConnect, которому передается объект класса sender. Присоединение обработчика события к событию объекта sender выполняется при каждом вызове метода OnConnect.

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

    class Receiver2
        {       
            void ListChanged(object sender, ChangedEventArgs args)
            {
                Console.WriteLine("Сообщение послал {0}, " +
                "элемент = {1}. " + " Сообщение получил: Receiver2 - ",
                args.Name, args.Item);
                if (args.Permit =(int)args.Item < 20)
                    Console.WriteLine("Изменения разрешаю");
                else Console.WriteLine("Изменения не разрешаю");
            }
            public void OnConnect(ListWithChangedEvent list)
            {
                list.Changed += new ChangedEventHandler(ListChanged);
                //list.Changed = new ChangedEventHandler(ListChanged);
            }
            public void OffConnect(ListWithChangedEvent list)
            {
                list.Changed -= new ChangedEventHandler(ListChanged);
                //list.Changed = null;
            }
        }//class Receiver2

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

    public void TestChangeList()
    {
       // Создаются два объекта, вырабатывающие события
       ListWithChangedEvent list1 = new ListWithChangedEvent("list1");
          ListWithChangedEvent list2 = new ListWithChangedEvent("list2");
    
       // Создаются два объекта классов Receiver1 и Receiver2,
       //способные обрабатывать события класса ListWithChangedEvent
       Receiver1 receiver1 = new Receiver1(list1);
       Receiver2 receiver2 = new Receiver2();
          receiver2.OnConnect(list1);
       receiver2.OnConnect(list2);
    
    // Работа с объектами, приводящая к появлению событий
       Random rnd = new Random();         
       list1.Add(rnd.Next(20)); list1.Add(rnd.Next(20));
          list1.Add(33); list1[1] = 17;   
       list2.Add(10);   list2[0] = 25;   
          list2.Clear();
    
       //Отсоединение обработчика событий
       receiver1.OffConnect();
       list1.Add(21);
          list1.Clear();
    }

    В тестирующей процедуре моделируется процесс работы с объектами, посылающими сообщения о событиях и принимающими эти сообщения. Два созданных объекта list1 и list2 посылают сообщения о событиях всякий раз, когда в список добавляется новый элемент или изменяется значение существующего элемента. Два созданных объекта receiver1 и receiver2 получают приходящие сообщения. Первый из них получает сообщения только от объекта list1, второй - от двух объектов list1 и list2. В некоторых ситуациях оба получателя сообщений "дают добро" на изменение элемента, в других ситуациях оба запрещают изменения.

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

    (рис 7.3) События в мире объектов list

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

    Как было сказано, каждое событие класса представляется полем этого класса. Если у класса много объявленных событий, а реально возникает лишь малая часть из них, то предпочтительнее динамический подход, когда память отводится только событиям, для которых есть связанные с ними обработчики события. Это несколько замедляет время выполнения, но экономит память. Решение зависит от того, что в данном контексте важнее - память или время. Для реализации динамического подхода в момент объявления события в языке предусмотрена возможность задания пользовательских методов 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;}
       }
       //Аналогично объявляются события Ev2, Ev3, Ev4
       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.

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

    class Receiver3
     {
         string name;
          public Receiver3(string name)
          {
              this.name = name;
          }
          public string Name { get { return name; } }
          public void OnConnect(ManyEvents me, int number)
          {
              switch (number)
              {
                  case 1: { me.Ev1 += new EventHandler(H1); break; }
                  case 2: { me.Ev2 += new EventHandler(H2); break; }
                  case 3: { me.Ev3 += new EventHandler(H3); break; }
                  case 4: { me.Ev4 += new EventHandler(H4); break; }
              }
          }
          public void H1(object s, EventArgs e)
          {
              Console.WriteLine("Слушатель " + name +
                  " : Событие Ev1");
          }
          public void H2(object s, EventArgs e)
          {
              Console.WriteLine("Слушатель " + name +
                  " : Событие Ev2");
          }
          public void H3(object s, EventArgs e)
          {
              Console.WriteLine("Слушатель " + name +
                  " : Событие Ev3");
          }
          public void H4(object s, EventArgs e)
          {
              Console.WriteLine("Слушатель " + name +
                  " : Событие Ev4");
          }
      }

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

    В тестирующей процедуре создаются один объект класса ManyEvents и два объекта класса Receiver3.

    public void TestEvents()
      {
          ManyEvents me = new ManyEvents();
          Receiver3 rev1 = new Receiver3("First");
          rev1.OnConnect(me, 1);
          rev1.OnConnect(me, 3);
          Receiver3 rev2 = new Receiver3("Second");
          rev2.OnConnect(me, 2);
          rev2.OnConnect(me, 3);
          me.SimulateEvs();
      }

    Объект rev1 слушает первое и третье события, приходящие от объекта me. Объект rev2 слушает второе и третье события, приходящие от объекта me. Метод Simulate зажигает первое и третье события. Слушатели сообщений получают уведомления о возникших событиях и реагируют должным образом. Результаты работы показаны на рис. 7.4.

    (рис 7.4) Объекты с множественными событиями

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

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

    Начнем с описания делегата, задающего событие "пожар" и построенного по всем правилам.

    public delegate void FireEventHandler(object sender, FireEventArgs e);

    А теперь рассмотрим свойства класса, определяющего новый город.

    /// <summary>
       /// Модель города с событиями
        /// и следящих за ними службами города
       /// </summary>
       public class NewTown
       {
          //свойства
        string townName;    //название города
          int buildings;       //число домов в городе
          int days;         //число дней наблюдения
          //городские службы
          Police policeman;
          Ambulance ambulanceman;
          FireDetect fireman;
          //события в городе
          public event FireEventHandler Fire;
            string[] resultService;      //результаты действий служб      
          //моделирование случайных событий
          private Random rnd = new Random();
          //вероятность пожара в доме в текущий день
            double fireProbability;

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

    /// <summary>
          /// Конструктор города
            /// Создает службы и включает наблюдения
            /// за событиями
          /// </summary>
          /// <param name="name">название города</param>
          /// <param name="buildings">число домов</param>
          /// <param name="days">число дней наблюдения</param>
          public NewTown(string name, int buildings, int days)
          {
           townName = name;
           this.buildings = buildings;
             this.days = days;
           fireProbability = 1e-3;
                //Создание служб
             policeman = new Police(this);
             ambulanceman= new Ambulance(this);
             fireman= new FireDetect(this);
                //Подключение к наблюдению за событиями
             policeman.On();
             ambulanceman.On();
             fireman.On();      
          }

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

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

    /// <summary>
    /// Зажигается событие.
    /// Поочередно вызываются обработчики события  
    /// </summary>
    /// <param name="e">
    /// входные и выходные аргументы события
    /// </param>
    protected virtual void OnFire(FireEventArgs e)
          {
        const string MESSAGE_FIRE =
                    "В городе {0} пожар! Дом {1}. День {2}-й";
        Console.WriteLine(string.Format(MESSAGE_FIRE, townName,
            e.Building, e.Day));
        if (Fire != null)
        {
            Delegate[] eventHandlers = 
                Fire.GetInvocationList();
            resultService = new string[eventHandlers.Length];
            int k = 0;
            foreach (FireEventHandler evHandler in
                eventHandlers)
            {
                evHandler(this, e);
                resultService[k++] = e.Result;           
          }
        }
      }

    Обратите внимание: метод GetInvocatonList возвращает массив объектов класса Delegate, который является абстрактным классом и родителем для классов событий, в частности для класса FireEventHandler. Получив этот массив, далее с ним можно работать привычным образом. В цикле по элементам массива вызывается очередной обработчик события, результаты его работы сохраняются в специально созданном массиве resultService.

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

    /// <summary>
    /// Моделирование жизни города
    /// </summary>
      public void LifeOurTown()
    {
          const string OK =
             "В городе {0} все спокойно! Пожаров не было.";
          bool wasFire = false;   
          for(int day = 1; day <= days; day++)
          for(int building = 1; building <= buildings; building++)
          {
                  if (rnd.NextDouble() < fireProbability)
                  {
                      FireEventArgs e = new FireEventArgs(building, day);
                      OnFire(e);
                      wasFire = true;
                      for (int i = 0; i < resultService.Length; i++)
                          Console.WriteLine(resultService[i]);
                  }
          }
          if (!wasFire)
              Console.WriteLine(string.Format(OK, townName));      
    }

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

    public abstract class Receiver
       {        
        protected NewTown town;
        protected Random rnd = new Random();
          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);
          }        
          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)
          {
                const string OK =
                    "Милиция нашла виновных!";
                const string NOK =
                    "Милиция не нашла виновных! Следствие продолжается.";
                if (rnd.Next(0, 10) > 6)
                    e.Result = OK;
                else e.Result = NOK;
          }
       }// class Police
       public class FireDetect : Receiver
       {
          public FireDetect (NewTown town): base(town){}        
        public override void It_is_Fire(object sender, FireEventArgs e)
        {
           const string OK =
                "Пожарные потушили пожар!";
           const string NOK =
                "Пожар продолжается! Требуется помощь.";
           if (rnd.Next(0, 10) > 4)
                e.Result = OK;
           else e.Result = NOK;
        }
       }// class FireDetect
       public class Ambulance : Receiver
       {
          public Ambulance(NewTown town): base(town){}      
        public override void It_is_Fire(object sender, FireEventArgs e)
        {
            const string OK =
                "Скорая оказала помощь!";
            const string NOK =
                "Есть пострадавшие! Требуются лекарства.";
            if (rnd.Next(0, 10) > 2)
                e.Result = OK;
            else e.Result = NOK;
        }
       }// class Ambulance

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

    /// <summary>
       /// Класс,задающий входные и выходные аргументы события
       /// </summary>
      public class FireEventArgs : EventArgs
       {      
        int building;
          int day;
          string result;
            //Доступ к входным и выходным аргументам
          public int Building
          { get{return building;} }
          public int Day
          { get{return day;}   }
          public string Result
          {
            get { return result; }
            set{result = value;} 
        }
          public FireEventArgs(int building, int day)
          {
             this.building = building; this.day = day;
          }
       }//class FireEventArgs

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

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

    public void TestLifeTown()
          {
           NewTown sometown = new NewTown("Канск", 20, 100); 
             sometown.LifeOurTown();         
          }

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

    (рис 7.5) События в жизни города и три обработчика

    Проекты

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

    Проект к данной лекции Вы можете скачать здесь.

    Специфика поведения объектов

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

    Хотя реализация методов у всех объектов одна, это не значит, что результат выполнения одного и того же метода, вызванного разыми объектами, будет один и тот же, поскольку выполнение метода зависит от свойств. Так что методы " Пробежать километр " и " Решить задачу " будут давать разные результаты для разных объектов класса " Человек ".

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

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

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

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

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

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

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

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

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

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

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

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

  • Вначале объявляется делегат - функциональный класс, задающий сигнатуру. Как отмечалось при рассмотрении делегатов, объявление делегата может быть помещено в некоторый класс, например, класс sender. Но чаще всего это объявление находится вне класса в пространстве имен. Поскольку одна и та же сигнатура может быть у разных событий, для них достаточно иметь одного делегата. Для некоторых событий можно использовать стандартные делегаты, встроенные в каркас. Тогда достаточно знать только их имена.
  • Если делегат определен, то в классе sender, создающем события, достаточно объявить событие как экземпляр соответствующего делегата. Это делается точно так же, как и при объявлении функциональных экземпляров делегата. Исключением является добавление служебного слова event. Формальный синтаксис объявления таков:
    [атрибуты] [модификаторы]event [тип, заданный делегатом] [имя события]
  • Есть еще одна форма объявления, но о ней чуть позже. Чаще всего атрибуты не задаются, а работа происходит с модификатором доступа - public. Приведу пример объявления делегата и события, представляющего экземпляр этого делегата:

    namespace Events
    {   
      public delegate void FireEvent(int day, int building);
       public class TownWithEvents
       {
          public event FireEvent fireEvent;
          …   
       }//TownWithEvents
          …
    }//namespace Events

    В этом примере в классе TownWithEvents введено событие. Делегат FireEvent описывает класс событий, сигнатура которых содержит два аргумента, характеризующих событие. Объект fireEvent в классе TownWithEvents является экземпляром класса, заданного делегатом. Поскольку он снабжен модификатором event, он является событием со всеми вытекающими отсюда последствиями. Модификатор доступа public делает событие доступным для клиентов класса, обрабатывающего это событие.

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

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

    /// <summary>
    /// "Зажигание" события Fire (пожар)
    /// </summary>
    /// <param name="time">время обнаружения пожара</param>
    /// <param name="build">
      /// здание, в котором произошел пожар </param>
    void OnFire(int day, int buildings)
    {
       if (fireEvent!=null)
          fireEvent(day, buildings);
    }

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

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

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

    string townName;
    int buildings;
    int days;
    double fireProbability;
    Random random=new Random();

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

    Приведу конструктор класса и метод-свойство, обеспечивающее доступ к одному из закрытых полей класса:

    /// <summary>
      /// Конструктор, инициализирующий поля класса
      /// </summary>
      /// <param name="name">название города</param>
      /// <param name="buildings">число домов</param>
      /// <param name="days">число дней жизни города</param>
      /// <param name="fireProbability">
      /// вероятность пожара в доме в текущий день</param>
          public TownWithEvents(string name, int buildings,
          int days, double fireProbability)
          {
          townName = name;
          this.buildings = buildings;
          this.days = days;
          this.fireProbability = fireProbability;
          }
    /// <summary>
      /// Доступ к полю townName
      /// </summary>
      public string TownName
      { get { return townName; } }

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

    /// <summary>
    /// Моделирование жизни города
    /// </summary>
    public void TownLife()
      {
            const string OK =
                "В городе все спокойно! Пожаров не было.";
            bool wasFire = false;         
         for(int day = 1; day < days; day++)
                for(int building = 1; building < buildings; building++)
                    if (random.NextDouble() < fireProbability)
                {                   
                   OnFire(day,building);
                   wasFire = true;
                }         
            if (!wasFire)
                Console.WriteLine(OK);            
      }

    Мы разобрались в том, как создаются события в классе sender. Давайте теперь рассмотрим классы, принимающие событие.

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

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

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

  • иметь обработчик события - процедуру, согласованную по сигнатуре с функциональным типом делегата, который задает событие;
  • иметь ссылку на объект, создающий событие, чтобы получить доступ к событию - event -объекту;
  • уметь присоединить обработчик события к event -объекту. Присоединение можно реализовать по-разному. Это можно делать непосредственно в конструкторе класса, которому передается в качестве аргумента объект, посылающий сообщения. В этом случае уже при создании объект, получающий сообщение, изначально готов принимать и обрабатывать сообщения о событиях. Такое решение хорошо, когда класс настроен на прием сообщений от одного объекта. В других ситуациях удобнее иметь специальный метод, которому передается объект класса sender. В этом методе и происходит присоединение обработчика события к событию того объекта, который передан методу.
  • Рассмотрим пример, демонстрирующий возможное решение проблем:

    /// <summary>
        /// Пожарная служба - класс Reciver
        /// Принимает и обрабатывает событие FireEvent
        /// пожар в городе, клиентом которого является класс.
        /// </summary>
        public class FireMen
        {
            /// <summary>
            /// Город, клиентом которого является класс FireMen
            /// </summary>
            private TownWithEvents MyNativeTown;
            public FireMen(TownWithEvents twe)
            {
                this.MyNativeTown = twe;
                MyNativeTown.fireEvent += new FireEvent(FireHandler);
            }
            /// <summary>
            /// Обработчик события "пожар в городе"
            /// </summary>
            /// <param name="day">день пожара</param>
            /// <param name="buildings">строение</param>
            private void FireHandler(int day, int buildings)
            {
               string message = 
                   "В городе {0} произошел пожар! " +
                   "В день {1}, в доме № {2}" +
                   "  Пожар потушен!";    
               Console.WriteLine(string.Format(message, MyNativeTown.TownName,
                  day, buildings));
            }
            public void GoOut()
            {
                MyNativeTown.fireEvent -= new FireEvent(FireHandler);
            }
        }//FireMan

    В классе Fireman есть ссылка на объект класса TownWithEvents, создающий события. Сам объект передается конструктору класса. В конструкторе метод класса, задающий обработку события, присоединяется к списку вызовов event -объекта. Возможность комбинирования делегатов в полной мере проявляется при работе с событиями. Благодаря этой возможности одно и то же событие будет обрабатываться методами, принадлежащими разным классам receiver.

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

    public void TestTown()
    {
        string townName = "Энск";
        int buildings = 1000;
        int days = 30;
        double fireProbability = 1e-4;
        TownWithEvents town = new 
            TownWithEvents(townName, buildings, days,fireProbability);
        FireMen fireMen = new FireMen(town);
        Console.WriteLine("События в городе " + townName);
        town.TownLife();
    }

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

    (рис 7.2) Моделирование жизни города

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

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

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

  • делегат, задающий тип события, должен иметь фиксированную сигнатуру из двух аргументов: delegate <Имя_делегата> (object sender, <Тип_аргументов> args) ;
  • первый аргумент задает объект sender, создающий сообщение. Второй аргумент args задает остальные аргументы - входные и выходные, - передаваемые обработчику. Тип этого аргумента должен задаваться классом, наследником класса EventArgs из библиотеки классов FCL. Если обработчику никаких дополнительных аргументов не передается, то для второго аргумента следует просто указать класс EventArgs, передавая null в качестве фактического аргумента при включении события;
  • рекомендуемое имя делегата - составное, начинающееся именем события, после которого следует слово EventHandler, например, FireEventHandler. Если никаких дополнительных аргументов обработчику не передается, то тогда можно вообще делегата не объявлять, а пользоваться стандартным делегатом с именем EventHandler.
  • Две проблемы с обработчиками событий

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

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

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

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

    list.Changed += new ChangedEventHandler(ListChanged);
    //list.Changed = new ChangedEventHandler(ListChanged);

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

    list.Changed -= new ChangedEventHandler(ListChanged);
    //list.Changed = null;

    С этим как-то нужно бороться. Ключевое слово " event " разрешает выполнять над событиями только операцию присоединения обработчика событий к списку вызовов " += " и обратную операцию отсоединения " -= ". Никаких других операций над событиями выполнять нельзя. Тем самым, к счастью, решается проблема игнорирования коллег. Ошибки некорректного поведения класса receiver ловятся еще на этапе трансляции. В приведенных примерах некорректные попытки работы закомментированы.

    Изменение аргументов

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

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

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

    Долгое время я считал, что проблема выходных аргументов вообще не может быть корректно решена и представляет "дыру" в языке C#. Об этом я писал в предыдущем издании книги по C#. К счастью, я ошибался и теперь могу привести корректное решение проблемы. Решение проблемы означает, что объект, зажигающий событие, может получить значения выходных аргументов от каждого из обработчиков события в отдельности. Как это сделать?

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

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

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

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

    // Объявление делегата
       public delegate void ChangedEventHandler(object sender,
                     ChangedEventArgs args);

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

    /// <summary>
    /// Дополнительные аргументы события
    /// </summary>
     public class ChangedEventArgs:EventArgs
    {
         string name;    //входной аргумент
         object item;    //входной аргумент
         bool permit;    //выходной аргумент
         public string Name
         { get { return name; } }    //только чтение
           public object Item
         { get { return (item); } }    //только чтение
       public bool Permit
       {
          get {return(permit);}
        set { permit = value; }  //чтение и запись
       }
       public ChangedEventArgs( string name, object item)
       {
             this.name = name; this.item = item; 
         }
       public ChangedEventArgs()
       {}
       }//class ChangedEventArgs

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

    У класса два конструктора: один - без аргументов, второму передаются входные аргументы.

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

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

    Класс sender

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

    /// <summary>
       /// Класс, создающий событие.
        /// Потомок класса ArrayList.
       /// </summary> 
       public class ListWithChangedEvent: ArrayList 
       {      
        string name;    //имя объекта   
          public event ChangedEventHandler Changed;   //событие
        bool permit;  //результат обработки события
          /// <summary> 
             /// Конструктор
           /// </summary>
           /// <param name="name">имя объекта</param>
           public ListWithChangedEvent(string name)
            { this.name = name; }        
            public string Name
            { get { return name; } }

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

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

    /// <summary>
    /// Процедура On, включающая событие
    /// </summary>
    /// <param name="args">аргументы события</param> 
    protected virtual void OnChanged(ChangedEventArgs args) 
    {
       if (Changed != null)
          Changed(this, args);
    }

    Процедура OnChanged соответствует ранее описанному образцу. Если список обработчиков не пуст, то зажигается событие - посылается сообщение всем обработчикам события. Синтаксически конструкция Changed(this, args) - это вызов списка методов, поскольку Changed - объект функционального типа, к которому прикреплен список последовательно работающих методов.

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

    protected virtual void OnChanged(ChangedEventArgs args) 
       {
         int countYes = 0, countNo = 0;
         if (Changed != null)
         {                
            foreach (ChangedEventHandler del in Changed.GetInvocationList())
            {
                 del(this, args);
                 if (args.Permit) countYes++;
                 else countNo++;
            }
            permit = (countYes >= countNo); 
         }
       }

    Метод GetInvocationList позволяет получить список обработчиков события, а цикл foreach - вызывать один обработчик за другим. После того, как обработчик завершится, можно понять, каково значение сформированного выходного аргумента события. В данном примере окончательное решение принимается по большинству голосов. Счетчики countYes и countNo считают голоса "за" и "против". Представленное здесь решение демонстрирует корректный способ работы с событиями, когда обработчикам необходимо передавать входные и выходные аргументы.

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

  • метод Add, добавляющий новый элемент в конец списка;
  • индексатор this, дающий доступ к элементу списка по индексу;
  • метод Clear, производящий чистку списка.
  • // Переопределяемые методы, вызывающие событие Changed
          public override int Add(object value) 
          {
             int index = -1;
             ChangedEventArgs evargs = 
                    new ChangedEventArgs(name, value);
             OnChanged(evargs);
             if (permit) 
                index = base.Add(value);         
             return index;
          }

    Обратите внимание на схему включения события в процедуре Add. Вначале создается объект evargs - аргументы события, который передается методу OnChanged. Этот метод поочередно вызовет обработчики события и сформирует итоговый результат их работы. Анализ переменной permit позволяет установить, получено ли разрешение на изменение значения. При истинности значения этой переменной вызывается родительский метод Add, осуществляющий изменение значения. Аналогично устроены и другие методы, в которых возникает событие Changed.

    public override void Clear() 
    {
       ChangedEventArgs evargs = 
        new ChangedEventArgs(name, 0);
       OnChanged(evargs);
       base.Clear();
    }
    
    public override object this[int index] 
    {
       set 
       {
          ChangedEventArgs evargs = 
                  new ChangedEventArgs(name, value);
          OnChanged(evargs);
              if (permit) 
             base[index] = value;         
       }
       get   {return(base[index]);}
    }

    Это достаточно типичная схема организации класса с событиями.

    Классы receiver

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

    class Receiver1 
       {
          private ListWithChangedEvent list;
            /// <summary>
            /// Конструктор 
            /// Присоединяет обработчик к событию
            /// </summary>
            /// <param name="list">объект, посылающий сообщение</param>
          public Receiver1(ListWithChangedEvent list) 
          {
             this.list = list;
             // Присоединяет обработчик к событию.
                list.Changed += new ChangedEventHandler(ListChanged);
          }         
          public void OffConnect() 
          {
             // Отсоединяет обработчик  
             list.Changed -= new ChangedEventHandler(ListChanged);
          }
       }//class Receiver1

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

    Давайте подробнее рассмотрим устройство обработчика события:

    private void ListChanged(object sender, ChangedEventArgs args) 
      {     
        Console.WriteLine("Сообщение послал {0}, " + 
        "элемент = {1}. " + " Сообщение получил: Receiver1 - ",
        args.Name, args.Item);
        if(args.Permit = (int)args.Item < 10)     
            Console.WriteLine("Изменения разрешаю");
        else Console.WriteLine("Изменения не разрешаю");
          }

    С входными аргументами все просто. Они используются для формирования сообщения и формирования принимаемого решения. Но стоит подробнее разобраться, как в данном конкретном случае формируется выходной аргумент. Свое решение "Разрешить или не разрешить изменение" обработчик принимает в зависимости от величины элемента ( item ). Свое решение обработчик события принимает независимо, без оглядки на методы, совместно с ним обрабатывающими то же событие.

    Класс Receiver2 в отличие от класса Receiver1 позволяет слушать и обрабатывать сообщения нескольких объектов класса sender. С конструктора класса снимается задача связывания события с обработчиком события. Ему не нужно теперь передавать объект класса sender. У класса появляется специальный метод OnConnect, которому передается объект класса sender. Присоединение обработчика события к событию объекта sender выполняется при каждом вызове метода OnConnect.

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

    class Receiver2
        {       
            void ListChanged(object sender, ChangedEventArgs args)
            {
                Console.WriteLine("Сообщение послал {0}, " +
                "элемент = {1}. " + " Сообщение получил: Receiver2 - ",
                args.Name, args.Item);
                if (args.Permit =(int)args.Item < 20)
                    Console.WriteLine("Изменения разрешаю");
                else Console.WriteLine("Изменения не разрешаю");
            }
            public void OnConnect(ListWithChangedEvent list)
            {
                list.Changed += new ChangedEventHandler(ListChanged);
                //list.Changed = new ChangedEventHandler(ListChanged);
            }
            public void OffConnect(ListWithChangedEvent list)
            {
                list.Changed -= new ChangedEventHandler(ListChanged);
                //list.Changed = null;
            }
        }//class Receiver2

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

    public void TestChangeList()
    {
       // Создаются два объекта, вырабатывающие события
       ListWithChangedEvent list1 = new ListWithChangedEvent("list1");
          ListWithChangedEvent list2 = new ListWithChangedEvent("list2");
    
       // Создаются два объекта классов Receiver1 и Receiver2,
       //способные обрабатывать события класса ListWithChangedEvent
       Receiver1 receiver1 = new Receiver1(list1);
       Receiver2 receiver2 = new Receiver2();
          receiver2.OnConnect(list1);
       receiver2.OnConnect(list2);
    
    // Работа с объектами, приводящая к появлению событий
       Random rnd = new Random();         
       list1.Add(rnd.Next(20)); list1.Add(rnd.Next(20));
          list1.Add(33); list1[1] = 17;   
       list2.Add(10);   list2[0] = 25;   
          list2.Clear();
    
       //Отсоединение обработчика событий
       receiver1.OffConnect();
       list1.Add(21);
          list1.Clear();
    }

    В тестирующей процедуре моделируется процесс работы с объектами, посылающими сообщения о событиях и принимающими эти сообщения. Два созданных объекта list1 и list2 посылают сообщения о событиях всякий раз, когда в список добавляется новый элемент или изменяется значение существующего элемента. Два созданных объекта receiver1 и receiver2 получают приходящие сообщения. Первый из них получает сообщения только от объекта list1, второй - от двух объектов list1 и list2. В некоторых ситуациях оба получателя сообщений "дают добро" на изменение элемента, в других ситуациях оба запрещают изменения.

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

    (рис 7.3) События в мире объектов list

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

    Как было сказано, каждое событие класса представляется полем этого класса. Если у класса много объявленных событий, а реально возникает лишь малая часть из них, то предпочтительнее динамический подход, когда память отводится только событиям, для которых есть связанные с ними обработчики события. Это несколько замедляет время выполнения, но экономит память. Решение зависит от того, что в данном контексте важнее - память или время. Для реализации динамического подхода в момент объявления события в языке предусмотрена возможность задания пользовательских методов 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;}
       }
       //Аналогично объявляются события Ev2, Ev3, Ev4
       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.

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

    class Receiver3
     {
         string name;
          public Receiver3(string name)
          {
              this.name = name;
          }
          public string Name { get { return name; } }
          public void OnConnect(ManyEvents me, int number)
          {
              switch (number)
              {
                  case 1: { me.Ev1 += new EventHandler(H1); break; }
                  case 2: { me.Ev2 += new EventHandler(H2); break; }
                  case 3: { me.Ev3 += new EventHandler(H3); break; }
                  case 4: { me.Ev4 += new EventHandler(H4); break; }
              }
          }
          public void H1(object s, EventArgs e)
          {
              Console.WriteLine("Слушатель " + name +
                  " : Событие Ev1");
          }
          public void H2(object s, EventArgs e)
          {
              Console.WriteLine("Слушатель " + name +
                  " : Событие Ev2");
          }
          public void H3(object s, EventArgs e)
          {
              Console.WriteLine("Слушатель " + name +
                  " : Событие Ev3");
          }
          public void H4(object s, EventArgs e)
          {
              Console.WriteLine("Слушатель " + name +
                  " : Событие Ev4");
          }
      }

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

    В тестирующей процедуре создаются один объект класса ManyEvents и два объекта класса Receiver3.

    public void TestEvents()
      {
          ManyEvents me = new ManyEvents();
          Receiver3 rev1 = new Receiver3("First");
          rev1.OnConnect(me, 1);
          rev1.OnConnect(me, 3);
          Receiver3 rev2 = new Receiver3("Second");
          rev2.OnConnect(me, 2);
          rev2.OnConnect(me, 3);
          me.SimulateEvs();
      }

    Объект rev1 слушает первое и третье события, приходящие от объекта me. Объект rev2 слушает второе и третье события, приходящие от объекта me. Метод Simulate зажигает первое и третье события. Слушатели сообщений получают уведомления о возникших событиях и реагируют должным образом. Результаты работы показаны на рис. 7.4.

    (рис 7.4) Объекты с множественными событиями

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

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

    Начнем с описания делегата, задающего событие "пожар" и построенного по всем правилам.

    public delegate void FireEventHandler(object sender, FireEventArgs e);

    А теперь рассмотрим свойства класса, определяющего новый город.

    /// <summary>
       /// Модель города с событиями
        /// и следящих за ними службами города
       /// </summary>
       public class NewTown
       {
          //свойства
        string townName;    //название города
          int buildings;       //число домов в городе
          int days;         //число дней наблюдения
          //городские службы
          Police policeman;
          Ambulance ambulanceman;
          FireDetect fireman;
          //события в городе
          public event FireEventHandler Fire;
            string[] resultService;      //результаты действий служб      
          //моделирование случайных событий
          private Random rnd = new Random();
          //вероятность пожара в доме в текущий день
            double fireProbability;

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

    /// <summary>
          /// Конструктор города
            /// Создает службы и включает наблюдения
            /// за событиями
          /// </summary>
          /// <param name="name">название города</param>
          /// <param name="buildings">число домов</param>
          /// <param name="days">число дней наблюдения</param>
          public NewTown(string name, int buildings, int days)
          {
           townName = name;
           this.buildings = buildings;
             this.days = days;
           fireProbability = 1e-3;
                //Создание служб
             policeman = new Police(this);
             ambulanceman= new Ambulance(this);
             fireman= new FireDetect(this);
                //Подключение к наблюдению за событиями
             policeman.On();
             ambulanceman.On();
             fireman.On();      
          }

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

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

    /// <summary>
    /// Зажигается событие.
    /// Поочередно вызываются обработчики события  
    /// </summary>
    /// <param name="e">
    /// входные и выходные аргументы события
    /// </param>
    protected virtual void OnFire(FireEventArgs e)
          {
        const string MESSAGE_FIRE =
                    "В городе {0} пожар! Дом {1}. День {2}-й";
        Console.WriteLine(string.Format(MESSAGE_FIRE, townName,
            e.Building, e.Day));
        if (Fire != null)
        {
            Delegate[] eventHandlers = 
                Fire.GetInvocationList();
            resultService = new string[eventHandlers.Length];
            int k = 0;
            foreach (FireEventHandler evHandler in
                eventHandlers)
            {
                evHandler(this, e);
                resultService[k++] = e.Result;           
          }
        }
      }

    Обратите внимание: метод GetInvocatonList возвращает массив объектов класса Delegate, который является абстрактным классом и родителем для классов событий, в частности для класса FireEventHandler. Получив этот массив, далее с ним можно работать привычным образом. В цикле по элементам массива вызывается очередной обработчик события, результаты его работы сохраняются в специально созданном массиве resultService.

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

    /// <summary>
    /// Моделирование жизни города
    /// </summary>
      public void LifeOurTown()
    {
          const string OK =
             "В городе {0} все спокойно! Пожаров не было.";
          bool wasFire = false;   
          for(int day = 1; day <= days; day++)
          for(int building = 1; building <= buildings; building++)
          {
                  if (rnd.NextDouble() < fireProbability)
                  {
                      FireEventArgs e = new FireEventArgs(building, day);
                      OnFire(e);
                      wasFire = true;
                      for (int i = 0; i < resultService.Length; i++)
                          Console.WriteLine(resultService[i]);
                  }
          }
          if (!wasFire)
              Console.WriteLine(string.Format(OK, townName));      
    }

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

    public abstract class Receiver
       {        
        protected NewTown town;
        protected Random rnd = new Random();
          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);
          }        
          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)
          {
                const string OK =
                    "Милиция нашла виновных!";
                const string NOK =
                    "Милиция не нашла виновных! Следствие продолжается.";
                if (rnd.Next(0, 10) > 6)
                    e.Result = OK;
                else e.Result = NOK;
          }
       }// class Police
       public class FireDetect : Receiver
       {
          public FireDetect (NewTown town): base(town){}        
        public override void It_is_Fire(object sender, FireEventArgs e)
        {
           const string OK =
                "Пожарные потушили пожар!";
           const string NOK =
                "Пожар продолжается! Требуется помощь.";
           if (rnd.Next(0, 10) > 4)
                e.Result = OK;
           else e.Result = NOK;
        }
       }// class FireDetect
       public class Ambulance : Receiver
       {
          public Ambulance(NewTown town): base(town){}      
        public override void It_is_Fire(object sender, FireEventArgs e)
        {
            const string OK =
                "Скорая оказала помощь!";
            const string NOK =
                "Есть пострадавшие! Требуются лекарства.";
            if (rnd.Next(0, 10) > 2)
                e.Result = OK;
            else e.Result = NOK;
        }
       }// class Ambulance

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

    /// <summary>
       /// Класс,задающий входные и выходные аргументы события
       /// </summary>
      public class FireEventArgs : EventArgs
       {      
        int building;
          int day;
          string result;
            //Доступ к входным и выходным аргументам
          public int Building
          { get{return building;} }
          public int Day
          { get{return day;}   }
          public string Result
          {
            get { return result; }
            set{result = value;} 
        }
          public FireEventArgs(int building, int day)
          {
             this.building = building; this.day = day;
          }
       }//class FireEventArgs

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

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

    public void TestLifeTown()
          {
           NewTown sometown = new NewTown("Канск", 20, 100); 
             sometown.LifeOurTown();         
          }

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

    (рис 7.5) События в жизни города и три обработчика

    Проекты

  • Создайте проект "Жизнь города", в котором происходят разные события.
  • Создайте проект "Жизнь факультета" с событием "День факультета".
  • Создайте проект "Жизнь факультета", в котором происходят разные события.
  • Создайте проект "Жизнь лошади".
  • Создайте проект "Жизнь автомобиля".
  • Создайте проект "Жизнь парохода".
  • Создайте проект "Жизнь студента".
  • Создайте проект "Работа конвейера".
  • Создайте проект "Военные действия". Классы, задающие противников, взаимно обрабатывают события друг друга. Например, объект одного класса зажигает событие "атака", обработчик этого события в другом классе в ответ зажигает событие "контратака".
  • Создайте проект "Быки и Медведи", где объекты класса "Быки" играют на бирже на повышение, а "Медведи" - на понижение.
  • Исследуйте возможность создания класса, в котором одни объекты этого класса создают события, а другие объекты этого же класса обрабатывают эти события.
  • Вернуться к учебному плану