Содержание
Событие — это член типа, с помощью которого этот тип (или его экземпляр) может уведомлять другие объекты о наступлении особых ситуациях (т.е. уведомлять о наступлении определенных «событий»), например, клик по кнопке, получение письма и т.д. Другие объекты, получив такое уведомление, смогут на него отреагировать, выполнив определенные действия. События — член типа, обеспечивающий такого рода взаимодействие объектов.
Таким образом, тип, в котором определено событие, должен позволять:
- регистрировать обработчики событий — статические или экземплярные методы, заинтересованные в получении уведомления о событии и выполняющие в ответ на это событие определенные действия;
- отменять регистрацию обработчиков событий;
- уведомлять зарегистрированные обработчики о том, что событие произошло.
Модель событий в C# основана на делегатах. По сути, событие — член типа, который ссылается на экземпляр любого объявленного ранее типа делегата. Регистрация обработчиков события осуществляется путем добавления методов-обработчиков в этот делегат. Отмена регистрации — путем удаления методов-обработчиков из делегата. А уведомление зарегистрированных обработчиков о наступлении события происходит путем вызова этого делегата.
Событие, как член типа, объявляется с помощью ключевого слова event
. Событию также назначается область видимости с помощью одного из модификаторов доступа, тип — любой ранее объявленный тип делегата, и имя — любой допустимый идентификатор. При возникновении события нужно просто вызвать член-событие объекта, ссылающийся на делегат с обработчиками:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | public delegate void PriceChangedHandler (decimal oldPrice, decimal newPrice); public class Stock { string symbol; decimal price; public Stock (string symbol) { this.symbol = symbol; } public event PriceChangedHandler PriceChanged; public decimal Price { get { return price; } set { if (price == value) return; if (PriceChanged != null) PriceChanged (price, value); price = value; } } } |
Особенность члена-события состоит в том, что даже если он объявлен с модификатором public
вызван как делегат он может быть только внутри родительского типа, т.к. только родительский тип может генерировать событие. Внешние объекты могут только добавлять и удалять из члена-события свои обработчики с помощью операторов +=
и -=
, как в обычный делегат.
Члены события могут быть виртуальными (virtual), переопределенными (overridden), абстрактными (abstract), запечатанными (sealed), а также статическими (static).
Стандартный шаблон события
.NET Framework предусматривает стандартный шаблон написания событий. Он предусматривает несколько моментов. Рассмотрим их по порядку на следующем примере:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | public class PriceChangedEventArgs : EventArgs { public readonly decimal LastPrice, NewPrice; public PriceChangedEventArgs (decimal lastPrice, decimal newPrice) { LastPrice = lastPrice; NewPrice = newPrice; } } public class Stock { string symbol; decimal price; public Stock (string symbol) { this.symbol = symbol; } public event EventHandler<PriceChangedEventArgs> PriceChanged; protected virtual void OnPriceChanged (PriceChangedEventArgs e) { if (PriceChanged != null) PriceChanged (this, e); } public decimal Price { get { return price; } set { if (price == value) return; OnPriceChanged (new PriceChangedEventArgs (price, value)); price = value; } } } |
Тип для хранения информации о событии
При возникновении события объект, в котором оно возникло, должен передать дополнительную информацию о событии объектам-получателям уведомления. Эта информация инкапсулируется в отдельный класс. По соглашению этот класс наследуется от типа System.EventArgs
, а имя его должно заканчиваться суффиксом EventArgs
.
1 2 3 4 5 6 7 8 | public class PriceChangedEventArgs : EventArgs { public readonly decimal LastPrice, NewPrice; public PriceChangedEventArgs (decimal lastPrice, decimal newPrice) { LastPrice = lastPrice; NewPrice = newPrice; } } |
Класс System.EventArgs
содержит всего один член (статическое поле Empty
) и служит базовым классом для новых типов, посредством которых передается информация о событии. Выглядит он примерно так:
1 2 3 4 5 | public class EventArgs { public static readonly EventArgs Empty = new EventArgs(); public EventArgs () {} } |
В большинстве случаев передавать дополнительную информацию о событии не требуется, т.к. ее просто нет. В таких случаях нет необходимости создавать класс для хранения информации о событии. Нет необходимости даже создавать экземпляр базового класса EventArgs
, можно просто воспользоваться его статическим полем Empty
, которое содержит ссылку на экземпляр класса.
Определение члена-события
Далее в соответствии с шаблоном необходимо определить класс объектов, которые будут генерировать события. Для этого, как отмечалось, нужно классу добавить член-событие.
1 | public event EventHandler<PriceChangedEventArgs> PriceChanged; |
Типом для члена-события выступает предопределенный в .NET Framework обобщенный делегат System.EventHandler<TEventArgs>
:
1 2 3 | public delegate void EventHandler<TEventArgs> (object sender, TEventArgs e) where TEventArgs : EventArgs; |
Делегат принимает два параметра. В качестве первого параметра source
в делегат передается сам объект, в котором произошло событие, т.е. при вызове делегата в него передается this
. В целях поддержки наследования передается он с базовым типом object
, а не с конкретным типом, как это сделано для второго параметра.
Второй параметр — это объект с информацией о событии (класс которого создавался на первом этапе). Ему уже указывается конкретный тип, который передается в качестве параметра типа в делегат (TEventArgs
). По соглашению в делегате и в методе-обработчике этот параметр называется e
.
Делегат EventHandler
возвращает void
, соответственно методы-обработчики тоже должны возвращать void
.
Метод, вызывающий член-событие
В соответствии с шаблоном в классе, генерирующем событие, должен быть виртуальный защищенный метод, вызываемый из кода класса и его потомков при возникновении события. По соглашению этот метод получает название, соответствующее маске On-имя-события
. Назначение метода в том, чтобы производные классы могли более гибко управлять вызовом члена-события, добавляя необходимый код до и после его вызова.
Метод принимает один параметр — объект с информацией о событии. Как минимум этот метод должен проверять, зарегистрированы ли в делегате члене-событии методы-обработчики, и если да, вызывать член-событие.
1 2 3 4 | protected virtual void OnPriceChanged (PriceChangedEventArgs e) { if (PriceChanged != null) PriceChanged (this, e); } |
Метод, генерирующий событие
У класса должен быть метод, принимающий некоторую входную информацию и в ответ генерирующий событие. Вместо метода это может быть свойство.
1 2 3 4 5 6 7 8 9 10 | public decimal Price { get { return price; } set { if (price == value) return; OnPriceChanged (new PriceChangedEventArgs (price, value)); price = value; } } |
Тип, отслеживающий событие
Теперь в любом типе мы можем добавлять методы обработчики события PriceChanged
нашего класса Stock
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | class Someclass { static void Main() { Stock stock = new Stock ("THPW"); stock.Price = 27.10M; stock.PriceChanged += stock_PriceChanged; // Добавляем обработчик stock.Price = 31.59M; } // Метод обработчик static void stock_PriceChanged (object sender, PriceChangedEventArgs e) { if ((e.NewPrice - e.LastPrice) / e.LastPrice > 0.1M) Console.WriteLine ("Alert, 10% price increase!"); } } |
Здесь, во-первых, создается метод обработчик. Он должен соответствовать делегату EventHandler
— возвращать void
и принимать два параметра: sender
и e
.
Sender
чаще всего просто игнорируется, но его можно использовать для взаимодействия с вызывающим объектом (т.е. тем объектом, в котором произошло событие, в нашем примере — объектом класса Stock
) и возвращать ему какую либо информацию, использовать его поля, свойства, вызывать его методы.
Второй параметр e
— объект с информацией о событии, которую предоставит вызывающий объект (объект, в котором произошло событие, в примере это Stock
).
Далее метод регистрируется в качестве обработчика путем добавления его член-событие экземпляра класса Stock:
1 | stock.PriceChanged += stock_PriceChanged; |
Отсутствие дополнительной информации о событии
Чаще всего нет необходимости передавать никакую дополнительную информацию о событии. В этом случае стандартный шаблон несколько изменяется:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | public class Stock { string symbol; decimal price; public Stock (string symbol) {this.symbol = symbol;} public event EventHandler PriceChanged; protected virtual void OnPriceChanged (EventArgs e) { if (PriceChanged != null) PriceChanged (this, e); } public decimal Price { get { return price; } set { if (price == value) return; price = value; OnPriceChanged (EventArgs.Empty); } } } |
Во-первых, нет нужды объявлять тип для хранения информации. Мы можем просто использовать базовый класс EventArgs
объявленный в .NET Framework.
Во-вторых можно использовать не обобщенный делегат EventArgs
.
И в-третьих, чтобы не создавать пустой объект типа EventArgs
, мы можем воспользоваться его статическим свойством Empty
, которое делает это за нас.
Средства доступа событий (Event Accessors)
Средства доступа событий являются всего лишь реализацией функций, выполняемых операторами делегатов +=
и -=
. По умолчанию средства доступа генерируются компилятором автоматически. Т.е. такое, например, объявления события:
1 | public event EventHandler PriceChanged; |
компилятор преобразует в частное поле-делегат и пару публичных функций средств доступа, которые будут реализовывать операторы += и -= применительно к частному полю-делегату.
1 2 3 4 5 6 | EventHandler _priceChanged; // Частный делегат public event EventHandler PriceChanged { add { _priceChanged += value; } remove { _priceChanged -= value; } } |
Однако эти действия компилятора можно переопределить, явно задав поле-делегат и средства доступа к нему из члена-события. В этом случае компилятор автоматически ничего проделывать не будет. Возможность явно задать средства доступа дает возможность более гибко управлять процессом добавления обработчиков в делегат.