Содержание
Событие — это член типа, с помощью которого этот тип (или его экземпляр) может уведомлять другие объекты о наступлении особых ситуациях (т.е. уведомлять о наступлении определенных «событий»), например, клик по кнопке, получение письма и т.д. Другие объекты, получив такое уведомление, смогут на него отреагировать, выполнив определенные действия. События — член типа, обеспечивающий такого рода взаимодействие объектов.
Таким образом, тип, в котором определено событие, должен позволять:
- регистрировать обработчики событий — статические или экземплярные методы, заинтересованные в получении уведомления о событии и выполняющие в ответ на это событие определенные действия;
- отменять регистрацию обработчиков событий;
- уведомлять зарегистрированные обработчики о том, что событие произошло.
Модель событий в 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; } } | 
Однако эти действия компилятора можно переопределить, явно задав поле-делегат и средства доступа к нему из члена-события. В этом случае компилятор автоматически ничего проделывать не будет. Возможность явно задать средства доступа дает возможность более гибко управлять процессом добавления обработчиков в делегат.

