Тверской государственный университет
Опубликован: 02.12.2009 | Доступ: свободный | Студентов: 2369 / 261 | Оценка: 4.47 / 4.24 | Длительность: 14:45:00
Лекция 8:

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

< Лекция 7 || Лекция 8: 12345 || Лекция 9 >

Классы 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 является выходным аргументом. Деление аргументов на входные и выходные выполняется на содержательном уровне и никак синтаксически не поддерживается. Разработчик класса, реализуя разные стратегии доступа к аргументам, тем самым разделяет их на входные и выходные. К входным аргументам открыт доступ только на чтение, так что ни один из обработчиков события не сможет изменить значения этих аргументов. Для выходного аргумента возможно как чтение, так и запись.

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

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

Правильно ли, что обработчик события принимает решение о допуске изменения элемента списка? Все зависит от контекста. В прошлые времена молодые могли объявить о своей помолвке, но требовалось разрешение родителей на брак. Времена изменились, теперь родительского благословения не требуется. Тем не менее, ситуации, требующие внешнего разрешения, встречаются довольно часто. У работника возникли события - получил приглашение на конференцию, появилась возможность заключения выгодного контракта. Во всех таких случаях требуется одобрение коллег и начальства. Заметьте, окончательное решение остается за объектом, пославшим сообщение.
< Лекция 7 || Лекция 8: 12345 || Лекция 9 >
Илья Ардов
Илья Ардов

Добрый день!

Я записан на программу. Куда высылать договор и диплом?

Дарья Федотова
Дарья Федотова
Михаил Ильенко
Михаил Ильенко
Россия
Александр Мигунов
Александр Мигунов
Россия