Основы, лучшие методы и соглашения реализации событий в C#
ОГЛАВЛЕНИЕ
Данная статья описывает основы, лучшие методы и соглашения реализации событий.
1. Введение
Все, что вам нужно, чтобы определить, реализовать и понять пользовательские события с помощью C#, изложено в данной статье. Представлены важнейшие компоновочные модули, которые должны использоваться для достижения данных целей, наряду с лучшими методами и соглашениями реализации событий. Данная статья описывает варианты .NET 1.x и 2.0+ для публикации и подписки на события.
Хотя поддержка реализации пользовательских событий была доступна с версии 1.0 среды разработки .NET, с тех пор были добавлены дополнительная связанная с событиями поддержка и возможности. Некоторые из новых возможностей (например, обобщенный System.EventHandler, безымянные методы, вывод делегатов и т.д.) образуют сокращения, предназначенные для упрощения реализации событий. Хотя такие методы ускоряют реализации событий, предъявление их до или вместо важнейших компоновочных модулей дало бы менее точное представление. Поэтому данная статья избегает таких сокращений до тех пор, пока не будут введены все важнейшие компоновочные модули.
Предположения об аудитории
Данная статья предполагает знания программирования .NET с C#, наряду с пониманием общих средств, внедренных в версию 2.0 среды разработки .NET. Если вы не понимаете общие средства, эта статья все же может быть полезной, так как в ней изложены способы реализации событий, не основанные на общих средствах. В данной статье представлены общие и не общие методы реализации событий.
2. Терминология и определения
Литература, описывающая события и связанные с ними идеи, часто использует множество слов или выражений для описания любого конкретного понятия. Следующий список перечисляет большую часть данной терминологии с краткими пояснениями идей, лежащих в основе выражений.
событие, предсобытие, постсобытие, и состояние, изменение состояния и ожидаемое изменение состояния
Термин "событие" обычно означает, что произошло или вскоре произойдет изменение в состоянии объекта. Термин также используется в отношении некоторых действий, совершающихся внутри объекта или приложения – таких действий, как обработка знака от конечного пользователя (например, нажата кнопка), или оповещение о прогрессе в течение длительной задачи.
Термин "состояние" ссылается на текущий набор значений одной или более переменных в объекте или приложении. Изменение в состоянии означает, что значение одной или более переменных внутри объекта изменилось. В процессе уведомления о событии изменения в состоянии, или ожидаемые изменения в состоянии, являются главными причинами возбуждения событий. Поэтому есть два способа определить событие, связанное с изменением в состоянии: непосредственно перед изменением в состоянии, или сразу после изменения в состоянии. Первые называются предсобытиями, а последние постсобытиями.
Постсобытия объявляют, что изменение в состоянии уже произошло, а предсобытия объявляют, что изменение в состоянии вскоре произойдет. Предсобытия могут быть реализованы как отменяемые – означает, что подписчик может отменить событие до того, как произойдет изменение в состоянии, тем самым не дав возникнуть изменению в состоянии, или запретив последующую обработку длительной задачи.
публикатор события, источник события, предмет
Это классы или объекты, состояние которых представляет интерес для других классов или объектов. Публикаторы событий сохраняют свое внутреннее состояние и уведомляют другие классы (подписчиков) посредством возбуждения событий или аналогичных механизмов уведомления.
подписчик события, получатель, слушатель, наблюдатель
Это классы или объекты, интересующиеся изменениями в состоянии (или ожидаемыми изменениями в состоянии) публикаторов событий. Эти термины ссылаются на классы или объекты, обычно выполняющие некоторое действие в ответ на возникновение события.
возбудить, запустить, или вызвать событие; уведомление, или уведомление о событии
Уведомления о событиях (часто выражаемые в виде, "запустить событие" или "возбудить событие" или "вызвать событие") обычно имеют форму публикатора события, вызывающего метод в одном или более подписчиков. Следовательно, возбуждение события в итоге означает, что код в публикаторе события вызывает выполнение кода в одном или более подписчиков.
В случаях, когда никакие подписчики [на событие] не были зарегистрированы в публикаторе, событие не возбуждается.
Заметьте, что в данной статье события описаны как "возбужденные" (не "запущенные" или "вызванные"). Это соглашение исходит от группы разработчиков, создавших большую часть среды разработки .NET (Квалина и Абрамс, 2006). Они предпочитают термин, "возбудить", так как он не имеет дополнительных негативных значений выражений "запустить" или "вызвать".
данные о событии, связанные с событием данные, и аргументы события ("event args")
Когда событие возбуждается, публикатор часто включает данные, отправляемые подписчикам посредством процесса уведомления о событии. Эти данные, вероятно, относятся к конкретному событию, которое было возбуждено, и представляют интерес для подписчиков событий.
Например, событие может быть возбуждено, когда файл переименовывается. Данные, относящиеся к данному конкретному событию "файл переименован", могут включать (1) имя файла до изменения имени, и (2) имя файла после изменения имени. Эти имена файлов могут образовать данные о событии, отправляемые подписчикам во время возбуждения события "файл переименован".
Тип делегата, делегаты
Ясное понимание типов делегатов .NET крайне важно для понимания способа реализации событий в среде разработки .NET. Следовательно, большая часть данной статьи посвящена объяснению связи между делегатами и событиями.
Два значения "обработчика события"
Литература за пределами данной статьи часто использует термин, "обработчик события", в отношении (1) делегата, на основе которого определено событие (в публикаторе), или (2) любого метода, зарегистрированного в событии (в подписчике). Более того, Intellisense("интеллектуальное восприятие") в Visual Studio ссылается на метод обработки события (в подписчике) просто как на "обработчик". Для ясности данная статья использует выражение "обработчик события" в отношении делегата, тогда как выражение "метод обработки события" используется в отношении любого метода, зарегистрированного в событии.
Для обобщения: "обработчик события" – делегат, на котором событие основано, тогда как "метод обработки события" – метод, вызываемый в подписчике, когда событие возбуждается.
Обработчики событий являются делегатами, несмотря на то, что делегаты не обязательно являются обработчиками событий (существует много применений делегатов, помимо поддержки событий). Делегаты более подробно описаны далее в этой статье, но лишь в той части, в какой они относятся к событиям.
События .NET и шаблон наблюдателя GoF
События, реализованные в среде разработки .NET и описанные в данной статье, образуют оптимизированную реализацию .NET шаблона наблюдателя, документированного "бандой четырёх" или "GoF" (Гамма и другие, 1995). Механизмы .NET, используемые для реализации событий (особенно делегатов), существенно сокращают объем работ, требуемый для реализации шаблона наблюдателя в приложениях .NET.
3. Делегаты
Чтобы понять события, реализованные в приложениях .NET, нужно ясно понимать тип делегата .NET и роль, которую он играет в реализации событий.
3.1 Определение и использование делегатов
Делегаты являются разумными контейнерами, хранящими ссылки на методы, в отличие от контейнеров, хранящих ссылки на объекты. Делегаты могут содержать ссылки на ноль, один или много методов. Чтобы метод был вызван конкретным экземпляром делегата, этот метод должен быть зарегистрирован в экземпляре делегата. При регистрации метод добавляется во внутреннюю коллекцию делегата, хранящую ссылки на методы (список вызова делегата). Делегаты могут хранить ссылки на статические методы или методы экземпляров в любом классе, видимом для экземпляра делегата. Экземпляры делегатов могут синхронно или асинхронно вызывать методы, на которые экземпляры ссылаются. При асинхронном вызове методы выполняются в отдельном потоке, потоке пула. Когда вызывается экземпляр делегата, все методы, на которые ссылается делегат, автоматически вызываются делегатом.
Делегаты не могут содержать ссылки на любой метод. Делегаты могут хранить ссылки только на методы, определенные с помощью сигнатуры метода, точно совпадающей с сигнатурой делегата.
Рассмотрим следующее объявление делегата:
public delegate void MyDelegate(string myString);
Заметьте, что объявление делегата выглядит как объявление метода, но без тела метода.
Сигнатура делегата определяет сигнатуру методов, на которые может ссылаться делегат. Пример делегата выше (MyDelegate) может хранить ссылки только на методы, возвращающие void, при этом принимая единственный строковый аргумент. Следовательно, следующий метод может быть зарегистрирован в экземпляре MyDelegate:
private void MyMethod(string someString)
{
// тело метода здесь.
}
Однако на следующие методы экземпляр MyDelegate не может ссылаться, так как их сигнатуры не совпадают с сигнатурой MyDelegate.
private string MyOtherMethod(string someString)
{
// тело метода здесь.
}
private void YetAnotherMethod(string someString, int someInt)
{
// тело метода здесь.
}
После объявления нового типа делегата должен быть создан экземпляр этого делегата, чтобы методы могли быть зарегистрированы в нем и в итоге вызваны экземпляром делегата.
// создается экземпляр делегата и регистрируется метод в новом экземпляре.
MyDelegate del = new MyDelegate(MyMethod);
После создания экземпляра делегата в нем могут быть зарегистрированы дополнительные методы, таким образом:
del += new MyDelegate(MyOtherMethod);
Теперь делегат можно вызвать так:
del("my string value");
Так как MyMethod и MyOtherMethod зарегистрированы в экземпляре MyDelegate (названным del), этот экземпляр вызовет MyMethod и MyOtherMethod, когда выполнится строка кода выше, передав каждому из методов строковое значение "мое строковое значение".
Делегаты и Перегруженные методы
В случае перегруженного метода делегат может ссылаться (или зарегистрировать в себе) только на конкретную перегрузку, имеющую сигнатуру, точно совпадающую с сигнатурой делегата. Когда вы пишете код, который регистрирует перегруженный метод в экземпляре делегата, компилятор C# автоматически выбирает и регистрирует конкретную перегрузку с совпадающей сигнатурой.
Так, например, если ваше приложение объявило следующий тип делегата...
public delegate int MyOtherDelegate(); // возвращает целое число, параметров нет
... и вы зарегистрировали перегруженный метод под названием MyOverloadedMethod в экземпляре MyOtherDelegate, таким образом...
anotherDel += new MyOtherDelegate(MyOverloadedMethod);
... компилятор C# зарегистрирует только конкретную перегрузку с совпадающей сигнатурой. Из следующих двух перегрузок только первая была бы зарегистрирована в экземпляре anotherDel типа MyOtherDelegate:
// не требует параметров – поэтому может быть зарегистрирована в экземпляре MyOtherDelegate
private int MyOverloadedMethod()
{
// тело метода здесь.
}
// требует строковый параметр – поэтому не может быть зарегистрирован в экземпляре MyOtherDelegate.
private int MyOverloadedMethod(string someString)
{
// тело метода здесь.
}
Единственный делегат не может избирательно регистрировать или вызывать обе (несколько) перегрузок. Если вам нужно вызывать обе (несколько) перегрузок, то вам понадобятся дополнительные типы делегатов – один тип делегата на каждую сигнатуру. Ваша относящаяся к приложению логика тогда определяла бы, какой делегат вызывать, и какая перегрузка вызывается (делегатом с соответствующей подписью).
3.2 В чем необходимость делегатов?
Если это ваше первое знакомство с делегатами, вы можете задаться вопросом: "Зачем утруждаться, не проще ли вызвать метод напрямую? Какая польза от применения делегата?"
Необходимая косвенность
Краткий ответ (на вопрос выше "зачем утруждаться?") заключается в том, что написанный нами код или используемые нами компоненты не могут всегда знать, какой конкретный метод вызывать в конкретный момент времени. Одна важная перспектива делегатов в том, что они предоставляют компонентам .NET способ вызова вашего кода без необходимости для компонента .NET знать что-либо о вашем коде, кроме сигнатуры метода (требуемой типом делегата). Например, компоненты среды разработки .NET, такие как компонент таймера, часто должны выполнять написанный вами код. Так как компонент таймера не может знать, какой конкретный метод вызывать, он задает тип делегата (и, стало быть, сигнатуру метода), который будет вызываться. Затем вы подключаете ваш метод – с требуемой сигнатурой – к компоненту таймера, регистрируя ваш метод в экземпляре делегата типа делегата, ожидаемого компонентом таймера. Компонент таймера затем может запустить ваш код, вызвав делегат, который, в свою очередь, вызовет ваш метод.
Заметьте, что компонент таймера по-прежнему ничего не знает о вашем конкретном методе. Компонент таймера знает только о делегате. Делегат, в свою очередь, знает о вашем методе, так как вы зарегистрировали ваш метод в этом делегате. Итог в том, что компонент таймера запускает ваш метод, ничего не зная о вашем конкретном методе.
Точно так же, как и пример компонента таймера выше, можно использовать делегаты таким образом, который позволяет вам писать ваш собственный код без необходимости для нашего кода знать конкретный метод, который в конечном счете будет вызван в конкретной точке. Вместо того чтобы вызывать метод в той точке, код может вызвать экземпляр делегата - который, в свою очередь, вызывает все методы, зарегистрированные в экземпляре делегата. Итог в том, что совместимый метод вызывается, хотя конкретный метод, который будет вызываться, не был непосредственно записан в нашем коде.
Синхронный и асинхронный вызов метода
Все делегаты, по сути, обеспечивают синхронный и асинхронный вызов метода. Еще одна распространенная причина вызывать методы через экземпляры делегата – вызывать методы асинхронно – при этом вызванный метод выполняется в отдельном потоке, потоке пула.
Основа события
Делегаты играют ключевую роль в реализации событий в среде разработки .NET. Вкратце, делегаты обеспечивают необходимый слой косвенности между публикаторами событий и их подписчиками. Данная косвенность нужна для сохранения полного разделения между публикатором и подписчиком(ами) – означает, что подписчики могут быть добавлены и удалены без необходимости каким-либо образом изменять публикатор. В случае публикации события использование делегата позволяет публикатору события ничего не знать ни об одном из его подписчиков, при этом транслируя события и связанные с событиями данные любому/всем подписчикам.
Другие применения
Делегаты играют важную роль в приложениях .NET, кроме уже перечисленных. Эти другие роли не будут дальше излагаться здесь, так как цель данной статьи – сосредоточиться только на основной роли, которую делегаты играют в реализации событий в приложениях .NET.
3.3 Внутреннее устройство делегатов
Объявление делегата приводит к созданию нового класса
Объявление делегата, которое вы напишете, достаточно для определения полного нового класса делегата. Компилятор C# берет ваше объявление делегата и вставляет новый класс делегата в выходную сборку. Имя этого нового класса – имя типа делегата, заданное вами в объявлении делегата. Сигнатура, заданная в вашем объявлении делегата, становится сигнатурой методов в новом классе, используемом для вызова любого/всех из методов, на которые ссылается делегат (особенно методы Invoke и BeginInvoke). Этот новый класс расширяет (наследует) System.MulticastDelegate. Поэтому большинство из методов и свойств, доступных в вашем новом классе делегата, взяты из System.MulticastDelegate. Методы Invoke, BeginInvoke и EndInvoke вставляются компилятором C#, когда он создает новый класс в выходной сборке (это методы, которые можно вызвать, чтобы заставить делегат вызвать любой/все методы, на которые он ссылается, - Invoke для синхронного вызова, и BeginInvoke и EndInvoke, используемые в асинхронных вызовах).
Новый класс, созданный на основе вашего объявления делегата, можно рассматривать как законченную и полноценную реализацию MulticastDelegate, имеющую имя типа, заданное в вашем объявлении делегата, и способную вызывать методы с конкретной сигнатурой, указанной в вашем объявлении делегата.
Например, когда компилятор C# встречает следующее объявление делегата...
public delegate string MyFabulousDelegate(int myIntParm);
... компилятор вставляет новый класс по имени MyFabulousDelegate в выходную сборку. Методы Invoke, BeginInvoke и EndInvoke класса MyFabulousDelegate содержат параметр int и возвращаемое значение string в своих соответствующих сигнатурах методов.
Нужно отметить, что MulticastDelegate – особый класс в том смысле, что компиляторы могут наследовать от него, но вы не можете явно наследовать от него. Ваше использование ключевого слова C# delegate и связанный с ним синтаксис являются методом приказания компилятору C# расширить MulticastDelegate для ваших целей.
Смысл многоадресности
Смысл многоадресности в System.MulticastDelegate в том, что делегат способен хранить ссылки на несколько методов, а не только на один метод. В случае экземпляров делегата, хранящих ссылки на несколько методов, все методы, на которые хранятся ссылки, вызываются, когда вызывается экземпляр делегата.
Делегаты неизменяемые
Экземпляры делегатов неизменяемые – то есть, как только экземпляр делегата создан, он не может быть изменен. Когда вы регистрируете метод в делегате, создается новый экземпляр делегата, включающий дополнительный метод в свой список вызова. Если вы разрегистрируете метод из экземпляра делегата, возвращается новый экземпляр делегата, из списка вызова которого исключен разрегистрированный метод. Если вам нужно создать новую объектную переменную конкретного типа делегата, приравняйте его к существующему экземпляру делегата (данного конкретного типа) – вы получите полноценную и отдельную копию делегата. Изменения в копии (например, регистрация дополнительного метода) повлияют только на копию. Список вызова исходного экземпляра останется неизменным.
Делегаты не являются указателями функций
Наконец, программисты C и C++ признают, что делегаты похожи на указатели функций в стиле C. Но важное отличие в том, что делегат – не просто указатель на непосредственный адрес памяти. Наоборот, экземпляры делегатов являются типизированными объектами, управляемыми .NET CLR(общеязыковая среда исполнения) и ссылаются на один или более методов (в отличие от адресов памяти).
3.4 Все делегаты одинаковые (отсутствуют радикально различающиеся типы делегатов)
Все эти утверждения верны:
"Если вы видели один делегат, вы видели их все".
или
"Все делегаты созданы равными"
или
"Делегат является делегатом является делегатом"
Когда вы читаете о разных типах делегатов, вы должны понимать, что внутри все делегаты одинаковы. Это верно для делегатов, предоставленных средой разработки .NET, и для делегатов, создаваемых вами для ваших собственных целей. Утверждение "все они одинаковые", в частности, означает, что все делегаты (1) наследуются от System.MulticastDelegate, который, в свою очередь, наследуется от System.Delegate; и (2) предоставляют одинаковый набор членов, включая методы Invoke,BeginInvoke, и EndInvoke() и т.д.
Типы делегатов различаются только по таким параметрам,как:
1. Имя типа делегата.
2. Сигнатура делегата - включая тип возвращаемой переменной и количество и типы параметров.
3. Предусмотренное применение или роль делегата.
Возьмем, например, обобщенный делегат предикат (System.Predicate<T>). Ниже перечислено то, что делает его "делегатом предикат":
1. Имя типа: Predicate
2. Сигнатура: возвращает bool, принимает единственный параметр типа object, для которого тип, будучи обобщенным, может быть установлен на этапе проектирования.
3. Предусмотренное применение или роль: этот делегат будет ссылаться на метод, определяющий набор критериев и устанавливающий, соответствует ли заданный объект данным критериям.
Кроме имени типа, сигнатуры и предусмотренного применения, делегат Predicate<T> имеет такой же набор членов, что и у любого другого делегата, включая Invoke, BeginInvoke, и т.д. Следовательно, это и означает утверждение, что "все делегаты одинаковые".
Дело не в том, что делегат Predicate<T> имеет какие-то дополнительные методы или свойства, помогающие ему выполнять его предусмотренную роль. Если бы некоторые делегаты имели свойства или методы, которые не имеют другие делегаты, то эти делегаты имели бы необычные или уникальные возможности, поэтому невозможно было бы сказать, что все они одинаковые.
Касательно перспективы предусмотренного применения: вы имеете право использовать любой делегат для целей, не предусмотренных создателями делегата – так как делегаты не связаны с каким-то конкретным применением. Вы можете, например, использовать делегат Predicate<T> для вызова любого метода, возвращающего bool и принимающего единственный параметр типа object – даже если эти методы не определяют, соответствует ли заданный объект какому-либо критерию (что является предусмотренным применением делегата Predicate<T>). Вы не должны использовать делегаты для целей, отличных от тех, которым они предназначены служить, так как ценность среды разработки .NET, предоставляющей предварительно подготовленные делегаты (такие как Predicate<T>) в том, что можно понять выполняемую ими роль без необходимости перерывать кучу кода, чтобы выяснить, что они на самом деле делают.
Имя типа делегата сообщает его предусмотренное применение в вашем коде. Поэтому обязательно используйте подходящий тип делегата, или создайте ваш собственный с информативным именем типа, даже если доступен другой делегат - с нужной сигнатурой, но с потенциально обманчивым именем с учетом вашего конкретного применения.
4. Связь между делегатами и событиями
События в программировании .NET основаны на делегатах. В частности, событие можно рассматривать как предоставляющее концептуальную обертку вокруг конкретного делегата. Событие контролирует доступ к этому лежащему под ним делегату. Когда клиент подписывается на событие, событие в конечном счете регистрирует подписывающийся метод в лежащем под ним делегате. Затем, когда событие возбуждается, лежащий под ним делегат вызывает каждый метод, зарегистрированный в нем (делегат). В случае событий делегаты играют роль посредников между кодом, вызывающим события, и кодом, выполняемым в ответ на них – тем самым отделяя публикаторов событий от их подписчиков.
События сами по себе не хранят список подписчиков. Наоборот, события контролируют доступ к некоторому лежащему под ними списку подписчиков – и этот список обычно реализован как делегат (хотя другие объекты или коллекции типа списка могут использоваться вместо делегатов).
4.1 Обработчики событий (в целом)
Делегат, существующий для обеспечения выполнения события, называется обработчиком события. Обработчик события является делегатом, хотя делегаты часто не являются обработчиками событий.
К сожалению, многие авторы, пишущие о событиях, используют термин "обработчик события" в отношении (1) делегата, на котором основано событие, и (2) метода, вызываемого делегатом, когда событие возбуждается. Чтобы избежать путаницы, обусловленной таким положением дел, эта статья использует выражение "обработчик события" только в отношении делегата, при этом используя выражение "метод обработки события" в отношении любого метода, зарегистрированного в делегате.
Пользовательские обработчики событий
Вы можете определить свои собственные обработчики событий (делегаты), или вы можете использовать один из обработчиков событий, предоставленных средой разработки .NET (т.е.System.EventHandler или обобщенный System.EventHandler<TEventArgs>). Следующий пример объявления события использует пользовательский обработчик события, а не предоставленный средой разработки обработчик события.
Рассмотрите следующее:
Line 1: public delegate void MyDelegate(string whatHappened);
Line 2: public event MyDelegate MyEvent;
Строка 1 объявляет тип делегата, для которого может быть назначен любой метод - при условии, что метод возвращает void и принимает единственный аргумент string:
• public – область видимости, указывающая, что объекты вне нашего класса могут ссылаться на делегат. Если тип делегата объявлен внутри класса публикации события, то он должен иметь общедоступную область видимости, чтобы подписчики события могли видеть его и объявлять его экземпляры, в которых они должны регистрировать свои методы обработки события (подробнее об этом позже).
• delegate – ключевое слово, применяемое для объявления пользовательских делегатов в среде разработки .NET.
• void - тип возвращаемой переменной. Это часть сигнатуры делегата, и соответственно тип возвращаемой переменной, который регистрирующие методы должны задавать.
• MyDelegate – имя типа делегата.
• (string whatHappened) – остальная часть сигнатуры. Любой метод, регистрирующийся в событии, должен принимать единственный аргумент string (наряду с возвращением void).
Строка 2 объявляет событие в виде типа делегата. Заметьте, что событие (названное MyEvent) объявлено очень похоже на объявление метода – но его тип данных задан как тип делегата:
• public - область видимости, указывающая, что объекты вне нашего класса могут подписываться на событие.
• event – ключевое слово, применяемое для определения события.
• MyDelegate – тип данных события (это пользовательский тип делегата, определенный в строке 1.)
• MyEvent – имя события.
Делегат, объявленный в строке 1, - всего лишь обычный делегат (как и все делегаты), и может использоваться для любой цели, которую делегаты могут выполнить. Строка 2 (т.е., использование типа делегата) превращает делегат в обработчик события. Чтобы сообщить, что конкретный тип делегата применяется в качестве обработчика события, появился способ именования, при котором имя типа делегата заканчивается на "обработчик" (подробнее об этом позднее).
Стандартизированные обработчики событий
Хотя вы можете создавать свои собственные обработчики событий (и иногда вам может это понадобиться), вы должны использовать один из делегатов EventHandler, предоставляемых средой разработки .NET, в случаях, когда один из обработчиков событий среды разработки может работать с вашей конкретной реализацией события. Многие события используют обработчики событий, имеющие общие или одинаковые сигнатуры. Поэтому не загромождайте ваш исходный код кучей делегатов, различающихся лишь по имени типа, а используйте встроенные обработчики событий, так как такой подход сокращает объем кода, который вам понадобится написать и обслуживать, и делает ваш код более понятным. Если некто, читающей ваш код, видит, что вы основываете событие на делегате System.EventHandler , то он автоматически много узнает о вашей реализации события без необходимости смотреть дальше.
4.2 Необобщенный делегат System.EventHandler
Доступный в версии 1.x среды разработки .NET, необобщенный делегат System.EventHandler навязывает условное обозначение (подробнее описано ниже) обработчиков событий, не возвращающих никакого значения, в то же время принимающих два параметра: первый является параметром типа object (чтобы хранить ссылку на класс, возбуждающий событие), второй параметр типа System.EventArgs или его подкласс (чтобы хранить любые данные о событии). System.EventArgs представлен далее.
Так среда разработки .NET объявляет делегат System.EventHandler.
public delegate void EventHandler(object sender, EventArgs e);
4.3 Обобщенный делегат System.EventHandler<TEventArgs>
Доступный начиная с версии 2.0 среды разработки .NET, обобщенный делегат System.EventHandler навязывает такое же соглашение сигнатуры, какое навязывает необобщенная версия, но принимает параметр обобщенного типа в качестве второго параметра System.EventArgs.
Объявление этого встроенного делегата навязывает ограничение, чтобы тип TEventArgs принадлежал к типу System.EventArgs (включая его подклассы):
public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e)
where TEventArgs : EventArgs;
Предположим, вам нужно строго задать тип отправителя, а не оставлять в качестве его типа object. Вы можете использовать обобщенные, чтобы создать ваш собственный обобщенный обработчик события:
public delegate void MyGenericEventHandler<T, U>(T sender,
U u) where U : EventArgs;
Вы можете использовать этот пользовательский обобщенный обработчик событий, чтобы дополнительно задать типизированный параметр sender (т.е. тем самым ограничивая тип объекта, который может быть передан в качестве возбудившего событие):
public event MyGenericEventHandler<MyPublisher, MyEventArgs> MyEvent;
Смысл в том, что это событие будет возбуждаться только объектами типа MyPublisher. Следовательно, подписчики события смогут подписываться только на события, опубликованные классом MyPublisher.
5. Аргументы события (EventArgs)
Аргументы события – иногда именуемые "event args" – образуют данные, отправляемые публикатором события подписчикам в ходе возбуждения события. Видимо, эти данные относятся к возникновению события. Например, когда возбуждается событие "файл был только что удален", аргументы события, скорее всего, будут состоять из имени файла до изменения имени, а также имени файла после изменения его имени. Методы обработки события могут считывать аргументы события (называемые "данные события"), чтобы узнать больше о возникновении события.
5.1 Роль System.EventArgs
Есть два основных варианта включения аргументов события в ваши события.
1. Вы можете инкапсулировать все аргументы события в виде свойств класса, полученного от System.EventArgs. Во время выполнения экземпляр этого класса отправляется подписчикам события, когда событие возбуждено. Подписчики события считывают аргументы события в виде свойств этого класса.
2. Вы можете избежать использования System.EventArgs и взамен объявить отдельные аргументы события - подобно тому, как вы включили бы аргументы в объявление метода. Причины невыгодности такого подхода описаны в разделе 5.2.
Первый указанный выше вариант настоятельно рекомендуется, и его поддержка встроена в среду разработки .NET с помощью класса System.EventArgs. События, реализованные в компонентах среды разработки .NET, по соглашению, предоставляют свои аргументы события в виде экземпляров System.EventArgs, или в виде специфичных для события подклассов System.EventArgs.
Некоторые события не содержат никаких данных. В этих случаях System.EventArgs используется как метка-заполнитель, в основном для сохранения одной единообразной сигнатуры обработчика события во всех событиях, независимо от того, содержат события данные или нет. В случае событий без данных публикатор события отправляет значение System.EventArgs.Empty во время возбуждения события.
5.2 Расширение System.EventArgs
Существование System.EventArgs и его рекомендуемые использования поддерживают соглашения о реализации событий. Безусловно, публикатор события может задавать данные события без использования System.EventArgs или любого его подкласса. В таких случаях сигнатура делегата может задавать тип и имя каждого параметра. Но недостаток данного подхода в том, что такая сигнатура связывает публикатор события со всеми подписчиками. Если вы хотите изменять параметры в дальнейшем, то вам также придется изменять всех подписчиков. Поэтому рекомендуется инкапсулировать все данные события в подклассе класса System.EventArgs, так как такой подход сокращает объем работы, требуемый для последующего изменения количества и типов значений, отправляемых подписчикам события.
Чтобы показать компромиссы, связанные с отправкой экземпляра подкласса System.EventArgs по сравнению с отправкой отдельных аргументов события, рассмотрим сценарий, в котором вам нужно добавить единственное значение типа string в данные события. Если бы вы задали данные события в виде отдельных параметров в сигнатуре делегата (а не путем создания производного класса от System.EventArgs), то все подписчики вашего события пришлось бы изменять, чтобы они принимали один дополнительный параметр string. Даже подписчики, не интересующиеся этим дополнительным значением string, пришлось бы изменить, чтобы они принимали это значение, так как сигнатура пользовательского обработчика событий изменилась бы. Если бы вместо этого вы создали производный класс от System.EventArgs, то вам пришлось бы лишь добавить новое свойство string в ваш класс. Сигнатура события не изменилась бы, и, следовательно, не изменилась бы сигнатура обработчика события, используемая в любом из существующих подписчиков. Подписчики, не интересующиеся новым свойством string, не пришлось бы изменять, так как сигнатура обработчика события не изменилась бы – и они могли бы игнорировать дополнительное свойство string. Подписчики, интересующиеся новым значением string, смогли бы считать его в виде свойства подкласса EventArgs.
Здесь приведен пример подкласса EventArgs, инкапсулирующего единственное значение string:
public class FileDeletedEventArgs : System.EventArgs
{
// Поле
string m_FileName = string.empty;
// Конструктор
FileDeletedEventArgs(string fileName)
{
m_FileName = fileName;
}
// Свойство
public string FileName
{
get { return m_FileName; }
}
}
5.3 Роль System.ComponentModel.CancelEventArgs
System.ComponentModel.CancelEventArgs получен от System.EventArgs, и существует для поддержки отменяемых событий. Кроме членов, предоставляемых EventArgs, CancelEventArgs предоставляет булево свойство Cancel, которое при установке в true подписчиком события, используется публикатором события для отмены события.
Раздел 16 данной статьи более подробно описывает отменяемые события (нажмите здесь, чтобы перейти туда сейчас).
6. Синтаксис объявления события
6.1 Варианты синтаксиса объявления события
Ключевое слово event используется для формального объявления события. Есть два правильных варианта синтаксиса объявления события. Независимо от того, какой синтаксис вы примените, компилятор C# преобразует оба объявления свойства в следующие три компонента в выходной сборке.
1. Обработчик события с закрытой областью действия (или функционально эквивалентная структура данных). Делегат имеет закрытую область действия, чтобы запретить вызов события внешним кодом, тем самым сохраняя инкапсуляцию.
2. Метод Add с открытой областью действия; используется для добавления подписчиков в закрытый обработчик события.
3. Метод Remove с открытой областью действия, применяемый для удаления подписчиков из закрытого обработчика события.
1. Полеподобный синтаксис
public event TheEventHandler MyEvent;
Полеподобный синтаксис объявляет событие в одной или двух строках кода (одна строка- для события, другая- для связанного с ним обработчика события, если/когда не используется встроенный делегат EventHandler).
2. Свойствоподобный синтаксис
public event TheEventHandler MyEvent
{
add
{
// код здесь добавляет входной экземпляр делегата в нижележащий список
// обработчиков события
}
remove
{
// код здесь удаляет экземпляр делегата из нижележащего списка
// обработчиков события
}
}
Свойствоподобный синтаксис выглядит очень похожим на типичное объявление свойства, но с явно заданными блоками add и remove вместо блоков "получатель" и "установщик". Вместо того, чтобы возвращать или устанавливать значение закрытой переменной члена, они добавляют и удаляют входные экземпляры делегата в/из нижележащего обработчика события или другой структуры данных, играющей аналогичную роль.
Рекомендации по организации поточной обработки
Полеподобный синтаксис автоматически является поточно-ориентированным:
public event FileDeletedHandler FileDeleted;
Свойствоподобный синтаксис придется специально делать поточно-ориентированным. Ниже приведен поточно-ориентированный вариант:
private readonly object padLock = new object();
public event System.EventHandler<filedeletedeventargs />FileDeleted
{
add
{
lock (padLock)
{
FileDeleted += value;
}
}
remove
{
lock (padLock)
{
FileDeleted -= value;
}
}
}
Вы можете опустить блоки lock{} и объявление переменной padlock, если потоковая безопасность не требуется.
6. 2 Рекомендации по выбору между полеподобным синтаксисом и свойствоподобным синтаксисом
Выбирая из вариантов синтаксиса, учитывайте, что свойствоподобный синтаксис дает вам больше контроля над реализацией вашего события, чем доступно при полеподобном синтаксисе. Хотя полеподобный синтаксис будет скомпилирован в IL, очень похожий на IL, сгенерированный для свойствоподобного синтаксиса, полеподобный синтаксис не дает вам таких же возможностей для явного контроля реализации события.
Использование свойствоподобного синтаксиса позволяет вам более тщательно контролировать регистрацию и отмену регистрации подписчиков в обработчике события (делегате). Оно также упрощает и делает более явной реализацию конкретного предпочтительного для вас механизма блокировки, чтобы решить вопросы потоковой безопасности.
Свойствоподобный синтаксис дополнительно позволяет вам реализовать собственный механизм обработчика события, отличный от делегата. Вам может понадобиться сделать это в сценариях, в которых вам нужно поддерживать много вероятных событий, из которых лишь несколько должны будут иметь каких-либо подписчиков в любой отдельно взятый момент времени. В таком сценарии ваша реализация события будет использовать хэш-таблицу или аналогичную структуру данных, а не отдельный делегат, чтобы хранить список всех возможных событий и любых связанных с ними слушателей.
6.3 Механизм публикации/подписки при помощи делегатов без событий (никогда не делайте так)
Должно быть ясно, что события не являются делегатами – хотя события сильно зависят от делегатов и в некоторых случаях могут рассматриваться как форма реализации делегата. События также не являются экземплярами делегатов, хотя события могут использоваться очень похожим образом.
Хотя вы можете пропустить ключевое слово "событие" (и, соответственно, формальное объявление события) и использовать открытый делегат для предоставления механизма уведомлений о публикации-и-подписке, вы никогда не должны так делать. Недостаток открытых делегатов (по сравнению с объявлением события) в том, что методы вне публикующего класса могут заставлять делегаты с открытой областью действия вызывать методы, на которые ссылаются данные делегаты. Это нарушает базовые принципы инкапсуляции и может быть источником серьезных трудно устранимых проблем (ситуация гонки, и т.д.). Поэтому вы должны реализовывать события только с помощью ключевого слова event. Когда делегаты реализуются для поддержки событий, делегат – даже если он объявлен в качестве открытого члена определяющего класса – может быть вызван только изнутри класса (путем возбуждения события), а другие классы могут только подписываться на и отписываться от нижележащего делегата с помощью события.
7. Код возбуждения события
Для каждого события публикатор должен содержать защищенный виртуальный метод, отвечающий за возбуждение события. Это облегчит подклассам доступ к событиям базового класса. Разумеется, рекомендация сделать этот метод защищенным и виртуальным применяется только для нестатических событий в открытых классах.
protected virtual void OnMailArrived(MailArrivedEventArgs)
{
// Здесь возбуждается событие
}
После того, как событие и любые связанные с ним делегаты и публикующий метод были определены, публикатор должен будет возбудить событие. Возбуждение события, как правило, должно быть двухэтапным процессом. На первом этапе проверяется наличие каких-либо подписчиков. На втором этапе возбуждается событие, но лишь при наличии каких-либо подписчиков.
Если подписчики отсутствуют, делегат будет проверен на null. Следующий код возбуждает событие, но только если событие имеет каких-либо подписчиков.
if (MyEvent != null)
{
MyEvent(this, EventArgs.Empty);
}
Есть вероятность, что событие может быть сброшено (кодом, выполняющимся в другом потоке) между проверкой на null и строкой, действительно возбуждающей событие. Этот сценарий создает ситуацию гонки. Поэтому рекомендуется создавать, проверять и возбуждать копию обработчика события (делегата) таким образом:
MyEventHandler handler = MyEvent;
if (handler != null)
{
handler (this, EventArgs.Empty)
}
Любые необработанные исключения, возбужденные в методах обработки события в подписчиках, будут переданы публикатору события. Поэтому возбуждение события должно выполняться только внутри блока try/catch:
public void RaiseTheEvent(MyEventArgs eventArgs)
{
try
{
MyEventHandler handler = MyEvent;
if (handler != null)
{
handler (this, eventArgs)
}
}
catch
{
// Здесь обрабатываются исключения
}
}
События могут иметь несколько подписчиков, каждый из которых по очереди вызывается обработчиком события (делегатом), когда обработчик события вызывается с помощью строки [handler (this, eventArgs)]. Обработчик события, используемый в вышеприведенном блоке кода, перестает повторять свой список вызовов (подписанных на него методов обработки события), когда первое необработанное исключение возбуждается подписчиком. Например, если имеется 3 подписчика, и 2-й подписчик выбрасывает необработанное исключение при вызове его делегатом, то 3-й подписчик вообще не получит уведомления о событии. Если вы хотите, чтобы все подписчики получали уведомление о событии, даже если другие подписчики выбрасывают необработанные исключения, воспользуйтесь следующим кодом, явно проходящим в цикле по списку вызовов обработчика события:
public void RaiseTheEvent(MyEventArgs eventArgs)
{
MyEventHandler handler = MyEvent;
if (handler != null)
{
Delegate[] eventHandlers = handler.GetInvocationList();
foreach (Delegate currentHandler in eventHandlers)
{
MyEventHandler currentSubscriber = (MyEventHandler)currentHandler;
try
{
currentSubscriber(this, eventArgs);
}
catch (Exception ex)
{
// Здесь обрабатывается исключение.
}
}
}
}
8. Регистрация и отмена регистрации подписчика события
Публикатор события заведомо абсолютно ничего не знает о любом из подписчиков. Следовательно, задача подписчиков – регистрировать или отменять регистрацию самих себя в публикаторе события.
8.1 Регистрация подписчика
Чтобы подписаться на событие, подписчику нужны три вещи:
1. Ссылка на объект, публикующий нужное ему событие
2. Экземпляр делегата, на основе которого определено событие
3. Метод, который будет вызван публикатором, когда он возбудит событие
После этого подписчик регистрирует свой экземпляр обработчика события (делегата) в публикаторе таким образом:
thePublisher.EventName += new
MyEventHandlerDelegate(EventHandlingMethodName);
В вышеприведенной строке:
• thePublisher – ссылка на объект, который будет возбуждать нужное событие. Обратите внимание, как событие EventName вызывается, словно оно является открытым свойством thePublisher.
• Оператор += используется для добавления экземпляра делегата в список вызовов обработчика события в публикаторе. Помните, что несколько подписчиков могут регистрироваться в событии. Используйте оператор += для добавления текущего подписчика в конец списка вызовов нижележащего делегата.
• MyEventHandlerDelegate – ссылка на конкретный делегат обработчика события, который будет использоваться (если это не один из встроенных делегатов EventHandler).
• Наконец, EventHandlingMethodName передает в класс-подписчик имя метода, который будет вызван при возбуждении события.
Предупреждение: Не используйте оператор = при регистрации подписчика события в публикаторе. Такой подход заменит любой/все зарегистрированные в настоящий момент подписчики события на текущий подписчик. Вместо этого обязательно используйте оператор +=, чтобы вызвать добавление текущего подписчика в конец списка вызовов обработчика события.
8.2 Отмена регистрации подписчика
Подписчика можно лишить регистрации в публикаторе таким образом:
thePublisher.EventName -=
EventHandlerDelegate(EventHandlingMethodName);
Оператор -= применяется для удаления экземпляра делегата из списка вызовов в публикаторе.
Регистрация подписчиков автоматически отменяется, когда объект уничтожается – если регистрация подписчика в событии еще не была отменена явно.
9. Метод обработки события
Метод обработки события - метод в подписчике события, выполняемый публикатором события при возбуждении события. Знайте, что некоторая литература, описывающая события в .NET, называет эти методы "обработчиками события", хотя, если быть технически точным, "обработчик события" - это делегат, на котором основано событие – а не любой метод, на который ссылается такой делегат.
Важное требование метода обработки события заключается в том, что его сигнатура должна совпадать с сигнатурой обработчика события (делегата), на основе которого определено событие.
Также вы должны тщательно рассмотреть последствия любых исключений, которые могут быть выброшены или захвачены в методе обработки события. Исключения, не захваченные в методе обработки события, будут переданы публикатору события.
10. Рекомендации по .NET 1.x в сравнении с 2.0+
Концепции и возможности, представленные в данном разделе, были внедрены в версию 2.0 среды разработки .NET. Эти новые возможности означают сокращение и, возможно, упрощение вашего кода при умелом использовании.
Но есть риск, что неправильное использование некоторых из этих возможностей может сделать малопонятным ваш код реализации события. Например, если вы воспользуетесь "безымянным методом" (изложен ниже), состоящим из 30+ строк кода, ваша реализация события, скорее всего, будет намного труднее для понимания, чем эквивалентная реализация, помещающая эти 30+ строк в именованный метод.
Важно понимать, что эти концепции и возможности 2.0+ не вносят никаких кардинальных изменений в способ реализации событий в приложениях среды разработки .NET. Наоборот, они предназначены для упрощения способа реализации событий.
10.1 Обобщенные
Наряду со специфичными для обобщенных возможностями, представленными в других местах данной статьи (например, System.EventHandler<T>), нужно заметить, что любые способы реализации событий, каким-либо образом основанные на обобщенных, не будут доступны в приложениях .NET 1.x, так как обобщенные были впервые введены в версии 2.0 среды разработки .NET.
10.2 Вывод делегатов
Компилятор C# 2.0 (и новее) достаточно разумен, чтобы определить тип делегата, на основе которого реализовано конкретное событие. Эта возможность "вывода делегатов" позволяет вам пропускать объявление требуемого делегата в коде, регистрирующем метод обработки события в событии.
Рассмотрим следующий код 1.x, регистрирующий метод обработки события в событии. Этот код явно создает экземпляр обработчика события (делегата), чтобы зарегистрировать связанный с ним метод в событии.
thePublisher.EventName += new MyEventHandlerDelegate(EventHandlingMethodName);
Следующий код 2.0+ использует вывод делегата для регистрации того же самого метода в событии. Заметьте, что следующий код выглядит регистрирующим метод обработки события непосредственно в событии.
thePublisher.EventName += EventHandlingMethodName;
При назначении имени метода непосредственно событию таким образом компилятор C# следит за тем, чтобы сигнатура метода совпадала с сигнатурой обработчика события, на котором основано событие. Затем компилятор C# вставляет нужный код регистрации делегата (т.е., ... += newMyEventHandlerDelegate(EventHandlingMethodName);) в выходную сборку.
Компилятор C# позволяет использовать этот упрощенный синтаксис, а не какие-либо изменения основных способов реализации событий в среде разработки .NET. События в C# 2.0 (и новее) не могут напрямую ссылаться на методы. Компилятор добавляет [все еще] требуемый синтаксис делегата в выходную сборку - словно мы явно создали экземпляр делегата.
10.3 Безымянные методы
Безымянный метод – блок кода, который вы передаете делегату (вместо передачи имени метода, на который будет ссылаться делегат). Когда компилятор C# встречает безымянный метод, он создает в выходной сборке полноценный метод, содержащий переданный вами блок кода. Компилятор задает имя для метода, затем создает ссылку на этот [новый] метод из связанного с ним экземпляра делегата (все это происходит в выходной сборке). Метод называется безымянным, потому что вы используете метод, не зная его имени (метод не имеет имени в вашем исходном коде).
Безымянные методы дают возможность писать более простой код. Рассмотрим следующий код, регистрирующий короткий метод обработки события в событии:
static void EventHandlingMethod(object sender, EventArgs e)
{
Console.WriteLine("Handled by a named method");
}
thePublisher.EventName += new MyEventHandlerDelegate(EventHandlingMethod);
Вышеуказанный код можно переписать с использованием безымянного метода так:
thePublisher.EventName += delegate {
Console.WriteLine("Handled by anonymous method");
};
Безымянные методы предназначены для упрощения кода. Упрощение может произойти, когда блок кода относительно короткий. В примере выше вариант кода, использующий синтаксис безымянного метода, легко читается, так как не нужно находить какой-то отдельный метод обработки события, чтобы понять, как подписчик будет реагировать при возбуждении события. Однако синтаксис безымянного метода может быть более тяжелым для понимания (более, чем код, ссылающийся на именованный метод) в случаях, когда блок кода состоит из множества строк кода. Некоторые авторы считают, что блоки кода, содержащие более трех или четырех строк кода, не должны реализовываться в виде безымянных методов. Эти более длинные блоки кода должны входить в именованные методы для повышения читабельности.
Чтобы обобщить представленные выше варианты, следующий код демонстрирует три варианта регистрации метода обработки события в событии. Первый демонстрирует явный подход, работающий во всех версиях среды разработки .NET. Второй демонстрирует вывод делегата. Третий демонстрирует применение безымянного метода:
// Вариант 1 – явное создание делегата с использованием именованного метода
thePublisher.EventName += new MyEventHandlerDelegate(EventHandlingMethod);
// Вариант 2 – вывод делегата
thePublisher.EventName += EventHandlingMethod;
// Вариант 3 – безымянный метод
thePublisher.EventName += delegate(object sender, EventArgs e) {
Console.WriteLine("handled by anonymous method");
// При необходимости вы можете здесь обращаться к отправителю и параметрам e
};
// Метод обработки события, используемый в вариантах 1 и 2
static void EventHandlingMethod(object sender, EventArgs e)
{
Console.WriteLine("Handled by a named method");
}
10.4 Частичные классы
Частичные классы имеют отношение к реализации событий в том, что Visual Studio поместит заглушки кода регистрации события и метода обработки события в файлы частичного класса, связанного с конкретным классом Windows Form. Нажмите здесь, чтобы перейти в раздел 15.1, подробнее описывающий реализацию событий в частичных классах.
11. Соглашения
Следующие соглашения были взяты из нескольких источников, включая авторов среды разработки .NET и других известных специалистов отрасли (полный список смотрите в библиографии в конце данной статьи).
11.1 Соглашения публикатора события
Имя события
• Выберите имя, четко передающее изменение состояния, обозначаемое событием.
• События могут быть разделены на следующие категории: (1) события, возбуждаемые до того, как происходит изменение состояния; и (2) события, возбуждаемые после того, как происходит изменение состояния. Соответственно, нужно выбирать имя события, говорящее, до или после изменения состояния возбуждается событие.
Примеры имен для событий, возбуждаемых до изменения состояния:
• FileDownloading
• TemperatureChanging
• MailArriving
Примеры имен для событий, возбуждаемых после изменения состояния:
• FileDownloadCompleted
• TemperatureChanged
• MailArrived
Производный класс System.EventArgs (где применимо)
• Для событий, которые должны или могут [когда-нибудь] содержать пользовательские данные о событии, вы должны создать новый класс, который (1) расширяет System.EventArgs, и (2) реализует члены (например, свойства), требуемые для того, чтобы содержать и предоставлять ваши пользовательские данные о событии
• Вы не должны создавать производный класс EventArgs, только если уверены, что ваше событие никогда не будет содержать данные о событии
• Именем вашего производного класса EventArgs должно быть имя события с добавленным в конец 'EventArgs'
Примеры имен производных классов от EventArgs:
• DownloadCompletedEventArgs
• TemperatureChangedEventArgs
• MailArrivedEventArgs
Для событий, которые не содержат, и никогда не будут содержать данных, рекомендуется передавать System.EventArgs.Empty. Эта рекомендуемая практика служит для соблюдения соглашений реализации событий даже для событий, не имеющих данных о событии. Если существует вероятность, что ваше событие может когда-нибудь содержать данные о событии, даже если не в первоначальной его реализации, то вы должны создать производный класс от System.EventArgs и использовать его в ваших событиях. Основная польза этой рекомендации в том, что вы однажды сможете добавить данные (свойства) в ваш производный класс, не нарушая совместимость с существующими подписчиками.
Имя обработчика события (делегата)
• Если вы используете среду разработки .NET 1.x, то вы должны использовать встроенный делегат System.EventHandler
• Если вы используете среду разработки .NET 2.0 или новее (для обоих публикаторов и подписчиков), то вы можете воспользоваться обобщенным делегатом System.EventHandler<TEventArgs>
• Если вы создаете свой собственный делегат, то имя делегата должно состоять из имени события с добавленным в конец словом 'Handler(обработчик)'
Примеры имен пользовательских обработчиков событий (делегатов):
• DownloadCompletedHandler
• TemperatureChangedHandler
• MailArrivedHandler
Сигнатура обработчика события (делегата)
Как сказано выше, в "имя делегата", вы должны использовать один из делегатов System.EventHandler, предоставленных средой разработки .NET. В этих случаях сигнатура делегата определяется для вас и автоматически соответствует рекомендуемым соглашениям.
Следующие рекомендации реализованы в делегатах System.EventHandler, предоставленных средой разработки .NET. Если вы создаете свои собственные обработчики событий, то вы должны следовать этим рекомендациям, чтобы сохранять соответствие с реализацией среды разработки .NET.
• Делегат всегда должен возвращать void.
В случае обработчиков событий нет смысла возвращать значение публикатору события. Помните, что публикаторы событий намеренно ничего не знают о своих подписчиках. Делегат намеренно выполняет функцию посредника между публикатором события и его подписчиками. Следовательно, публикаторы не обязаны ничего знать о своих подписчиках – включая вероятность получения возвращаемых значений. Делегат вызывает каждого подписчика, поэтому любое возвращаемое значение доберется только до делегата и, все равно, никогда не дойдет до публикатора. Этот принцип распространяется на избегание выходных параметров, использующих модификаторы параметра out или ref. Выходные параметры подписчиков никогда не дойдут до публикатора.
• Первый параметр должен иметь тип object и должен называться sender.
Этот первый параметр используется для хранения ссылки на объект, возбуждающий событие. Передача ссылки на публикатор события позволяет подписчику события различать между несколькими вероятными публикаторами конкретного события. Без ссылки на публикатор метод обработки события никак не сможет распознать или действовать в соответствии с конкретным публикатором, возбудившим конкретное событие.
Тип данных для sender – объект, так как практически любой класс может возбуждать события. Отсутствие строгой типизации параметра sender позволяет применять одну единственную сигнатуру обработчика события для всех событий. При необходимости метод обработки события может привести параметр отправителя к конкретному типу публикатора события.
Статические события должны передавать null в качестве значения отправителя, а не пропускать параметр sender.
• Второй параметр должен называться 'e' и должен иметь тип System.EventArgs или тип вашего собственного производного класса от System.EventArgs (например,MailArrivedEventArgs).
В случае отменяемых событий второй параметр имеет тип System.ComponentModel.CancelEventArgs или тип вашего собственного производного от него класса. В случае событий, не содержащих данных о событии, вы должны задать System.EventArgs в качестве типа второго параметра. В таких случаях System.EventArgs.Empty указывается в качестве значения этого параметра, когда событие возбуждается. Эта практика рекомендуется для сохранения соответствия соглашению – чтобы все сигнатуры обработчиков событий включали в себя параметр EventArgs – даже для событий, не имеющих EventArgs. Согласно соглашению, иметь одну единообразную сигнатуру более важно, чем иметь несколько сигнатур обработчиков событий – даже в случаях, когда один из параметров никогда не будет использоваться.
Примеры (без пользовательских данных, отправляемых вместе с событием):
delegate void DownloadCompletedHandler(object sender, EventArgs e);
delegate void TemperatureChangedHandler (object sender, EventArgs e);
delegate void MailArrivedHandler (object sender, EventArgs e);
Примеры (с пользовательскими данными, отправляемыми вместе с событием):
delegate void DownloadCompletedHandler(object sender,
DownloadCompletedEventArgs e);
delegate void TemperatureChangedHandler (object sender,
TemperatureChangedEventArgs e);
delegate void MailArrivedHandler (object sender,
MailArrivedEventArgs e);
Объявление события
• При условии, что событие подлежит предоставлению в распоряжение кода вне публикующего класса, событие должно объявляться с ключевым словом public (чтобы сделать его доступным для кода вне публикующего класса).
• Обработчик события, на котором основано событие, указывается в качестве типа события – аналогично тому, как тип данных указывается в типичном объявлении свойства или метода.
Пример (использует встроенный обобщенный делегат System.EventHandler<TEventArgs>):
public event System.EventHandler<mailarrivedeventargs> MailArrived;
Пример (использует пользовательский обработчик события):
public delegate void MailArrivedHandler (object sender,
MailArrivedEventArgs e);
public event MailArrivedHandler<mailarrivedeventargs> MailArrived;
Метод, возбуждающий событие
• Вместо возбуждения события, встраиваемого в весь ваш код многократно, рекомендуется создавать отдельный метод, отвечающий за возбуждение события. Затем вы вызываете этот метод во всем вашем коде при необходимости.
• Именем этого метода должно быть слово On с добавленным в конец именем события.
• Если ваше событие использует пользовательский подкласс EventArgs, то метод, возбуждающий событие, должен принимать как минимум один параметр, принадлежащий к конкретному подклассу EventArgs, определенному для пользовательских данных события.
• Для нестатических открытых классов метод должен быть реализован в виде virtual с доступностью, установленной в protected(защищенный), чтобы производные классы легко могли оповещать клиентов, зарегистрированных в базовом классе.
• Для закрытых классов доступность метода должна быть установлена в private(закрытый), так как возбуждение событий не должно запускаться извне класса.
Примеры (каждый принимает тип пользовательского подкласса EventArgs в качестве аргумента):
OnDownloadCompleted(DownloadCompletedEventArgs)
{
// Здесь возбуждается событие
}
private OnTemperatureChanged(TemperatureChangedEventArgs)
{
// Здесь возбуждается событие
}
virtual OnMailArrived(MailArrivedEventArgs)
{
// Здесь возбуждается событие
}
11.2 Соглашения подписчика события
Имя метода обработки события
• Соглашение, реализуемое Visual Studio, когда она автоматически создает заглушку метода обработки события, заключается в том, чтобы называть метод так: (1) имя объекта, возбуждающего событие; за ним следует (2) символ подчёркивания; (3) в конец добавляется имя события.
Примеры:
• downloader_DownloadCompleted
• weatherStation_TemperatureChanged
• mailManager_OnMailArrived
• Другое соглашение по заданию имени метода обработки события совпадает с описанным выше для задания имени метода, возбуждающего событие в публикаторе. В частности, именем метода должно быть слово On с добавленным в конец именем события.
Примеры:
• OnDownloadCompleted
• OnTemperatureChanged
• OnMailArrived
Сигнатура метода обработки события
• Сигнатура метода обработки события должна точно совпадать с сигнатурой делегата. Согласно соглашениям обработки событий, а также делегатов EventHandler, предоставленных средой разработки .NET, метод обработки события должен возвращать void, при этом принимая ровно два параметра: переменная типа object, имеющая имя sender, и экземпляр EventArgs (или производного класса), имеющий имя 'e'.
Примеры:
void DownloadManager_DownloadCompleted(object sender,
DownloadCompletedEventArgs e)
{
// здесь идет код обработки события
}
void WeatherStation_TemperatureChanged(object sender,
TemperatureChangedEventArgs e)
{
// здесь идет код обработки события
}
void MailMonitor_MailArrived(object sender, MailArrivedEventArgs e)
{
// здесь идет код обработки события
}
Подписка на событие (код, регистрирующий метод обработки события в событии)
• Чтобы зарегистрировать метод в событии, используйте синтаксис +=, согласно этому образцу:
EventPublisherObject.EventName += new EventHandlerDelegateName(NameOfMethodToCall);
Пример:
m_MailMonitor.MailArrived += new EventHandler(
this.MailMonitor_MailArrived);
Предупреждение: Не используйте оператор = при регистрации подписчика события в публикаторе. Такой способ заменит любые/все зарегистрированные в данный момент подписчики события на текущий подписчик. Вместо этого обязательно используйте оператор +=, чтобы добавить текущий подписчик в конец списка вызовов обработчика события.
Отмена подписки на событие (код, отменяющий регистрацию метода обработки события в событии)
• Чтобы отменить регистрацию метода на событие, используйте синтаксис -=, согласно этому образцу:
EventPublisherObject.EventName -= new EventHandlerDelegateName(NameOfMethodToCall);
Пример:
m_MailMonitor.MailArrived -= new EventHandler(
this.MailMonitor_MailArrived);
11.3 Соглашения об именовании
Горбатый регистр
Горбатый регистр – соглашение об именовании, при котором первая буква имеет нижний регистр, а каждая последующая "часть слова" начинается с буквы верхнего регистра. По соглашению, имена переменных пишутся в горбатом регистре.
Примеры горбатого регистра: someStringToWrite, ovenTemperature, latitude
Регистр Паскаля
Регистр Паскаля - соглашение об именовании, при котором каждая "часть слова" имени начинается с буквы верхнего регистра, с остальными буквами нижнего регистра, и без символов подчеркивания. По соглашению, имена классов, событий, делегатов, методов, и свойства пишутся в регистре Паскаля.
Примеры регистра Паскаля: MailArrivedEventHandler, AppClosing, MyClassName
12. Шаги к созданию пользовательских событий
Чтобы сделать следующие шаги максимально краткими, дается минимум или вообще не даются пояснения каждого конкретного шага. Объяснения, примеры и соглашения для каждого шага представлены в других местах по всей статье.
12.1 Подготовка публикатора события
Шаг 1: EventArgs – Решите, как ваше событие будет вызывать EventArgs.
• Включение EventArgs в пользовательские события необходимо для соответствия стандартам публикации события. Однако EventArgs не является техническим требованием – можно создавать, возбуждать и обрабатывать пользовательские события, вообще не использующие EventArgs.
• Если ваше событие никогда не будет передавать пользовательские данные события, то вы можете воспользоваться встроенным классом System.EventArgs. Позже вам придется задать значение EventArgs.Empty при возбуждении события.
• Если ваше событие не отменяемое и содержит пользовательские данные события, то вы должны создать класс, расширяющий System.EventArgs. Ваш пользовательский подкласс EventArgs должен содержать любые дополнительные свойства, содержащие данные события.
• Если ваше событие отменяемое, то вы можете использовать System.ComponentModel.CancelEventArgs – включающий в себя булево свойство Cancel, которое клиенты могут установить в истину, чтобы отменить событие. Вы можете создать производный класс от CancelEventArgs, содержащий свойства для любых дополнительных связанных с событием данных.
Шаг 2: Обработчик события – Решите, какой обработчик события ваше событие будет использовать.
• У вас есть два основных варианта – создать свой собственный обработчик события (делегат) или использовать один из делегатов EventHandler, предоставляемых средой разработки .NET. Если вы используете один из встроенных обработчиков событий, то вам придется поддерживать меньше кода, и сигнатура вашего обработчика события будет автоматически соответствовать соглашению возвращения void, при этом принимая параметры object sender, и EventArgs e
• При использовании .NET 1.x обдумайте использование встроенного делегата System.EventHandler.
• При использовании .NET 2.0 обдумайте использование встроенного обобщенного делегата System.EventHandler<TEventArgs>.
Шаг 3: Объявление события – Решите, какой синтаксис использовать: полеподобный синтаксис или свойствоподобный синтаксис.
• Полеподобного синтаксиса будет достаточно для многих реализаций пользовательских событий.
• Обдумайте применение свойствоподобного синтаксиса, если ваш класс предоставляет большое число событий, лишь на несколько из которых, как ожидается, будут подписываться в любой конкретный момент времени.
Шаг 4: Метод, возбуждающий событие – Решите, будете ли вы возбуждать событие из метода, или будете возбуждать его непосредственно в коде.
• Как правило, рекомендуется возбуждать события из метода, специально предназначенного для этой задачи, а не возбуждать события непосредственно в коде.
Шаг 5: Возбуждение события.
• Возбуждайте событие непосредственно в коде или вызывайте метод, возбуждающий событие.
• Перед возбуждением события вы должны создать экземпляр вашего подкласса EventArgs, заполненный связанными с событием данными. Если вы не используете никакой подкласс EventArgs, то вы должны добавить System.EventArgs.Empty вместо пользовательского класса EventArgs, когда вы возбуждаете метод.
12.2 Подготовка подписчика события
Так как эта статья дает образец события (object sender, EventArgs e), реализованного во всех классах среды разработки .NET, следующие шаги помогут вам подключить методы обработки события, работающие почти со всеми событиями среды разработки .NET, в дополнение к пользовательским событиям, которые вы создаете по тому же самому образцу.
Шаг 1: Написание метода обработки события.
• Определите метод обработки события с сигнатурой, точно совпадающей с сигнатурой делегата, на основе которого определяется метод.
• При использовании встроенного необобщенного делегата System.EventArgs или обобщенного делегата System.EventHandler<TEventArgs> в объявлении события, результирующая сигнатура автоматически соответствует соглашению возвращения void и принятия параметров (object sender, EventArgs e).
Шаг 2: Создание экземпляра публикатора события.
• Объявите переменную-член уровня класса, ссылающуюся на класс или объект, публикующий нужное событие.
Шаг 3: Создание экземпляра обработчика события (при необходимости).
• Если нужное событие основано на пользовательском обработчике события, создайте экземпляр этого обработчика события, передав имя метода обработки события.
• Этот шаг можно объединить с шагом 4 (следующим) путем использования ключевого слова new(новый) для создания экземпляра делегата в той же строке, в которой делегат регистрируется в событии.
Шаг 4: Регистрация подписчика (метода обработки события) в событии.
• Любая версия .NET: используйте синтаксис +=, чтобы зарегистрировать обработчик события в событии.
• .NET 2.0+: Как вариант, с помощью вывода делегата, вы можете непосредственно назначить событию имя метода.
• .NET 2.0+: Как вариант, если метод обработки события очень короткий (примерно 3 строки кода), вы можете сделать вашу реализацию более читабельной, зарегистрировав код обработки события с помощью "безымянного метода".
Шаг 5: Отмена регистрации подписчика (метода обработки события) в событии.
• Если подписчик больше не должен получать уведомления о событии от публикатора, вы можете отменить регистрацию подписчика в событии.
• Этот шаг можно считать необязательным, при условии, что подписчики автоматически лишаются регистрации в публикаторах, когда подписчик уничтожается.
13. Реализация типового события
Этот пример использует пользовательский обработчик события, хранит данные события в пользовательском подклассе EventArgs, объявляет событие с использованием полеподобного синтаксиса, и иным образом соответствует рекомендуемым стандартам и принципам реализации события. Ни одна из возможностей .NET 2.0+ (безымянные методы и т.д.) не используются для сохранения максимальной ясности изложения.
Это типовое событие возбуждается, когда файл перемещается утилитой для перемещения файлов (в примере проекта). Данные события содержат (1) имя перемещенного файла, (2) путь к исходной папке, и (3) путь к файлу назначения.
13.1 Код публикатора типового события
Шаг 1: Производный класс от EventArgs
Здесь мы порождаем новый класс, MoveFileEventArgs, от EventArgs, чтобы инкапсулировать данные события, отправляемые подписчику.
public class MoveFileEventArgs : EventArgs
{
// Поля
private string m_FileName = string.Empty;
private string m_SourceFolder = string.Empty;
private string m_DestinationFolder = string.Empty;
// Конструктор
public MoveFileEventArgs(string fileName, string sourceFolder,
string destinationFolder)
{
m_FileName = fileName;
m_SourceFolder = sourceFolder;
m_DestinationFolder = destinationFolder;
}
// Свойства (неизменяемые)
public string FileName
{
get { return m_FileName; }
}
public string SourceFolder
{
get { return m_SourceFolder; }
}
public string DestinationFolder
{
get { return m_DestinationFolder; }
}
}
Шаг 2: Обработчик события (делегат)
Здесь мы объявляем новый делегат, соответствующий соглашениям события, возвращающий void и принимающий два параметра; object, имеющий имя 'sender', и EventArgs, имеющий имя 'e'.
public delegate void MoveFileEventHandler(object sender,
MoveFileEventArgs e);
Шаг 3: Объявление события
Здесь мы объявляем событие с использованием полеподобного синтаксиса.
public event MoveFileEventHandler MoveFile;
Шаг 4: Метод, возбуждающий событие
Здесь мы объявляем метод, возбуждающий событие.
private void OnMoveFile()
{
if (MoveFile != null) // будет пустым, если нет подписчиков
{
MoveFile(this, new MoveFileEventArgs("SomeFileName.txt",
@"C:\TempSource", @"C:\TempDestination"));
}
}
Шаг 5: Возбуждение события
У нас есть метод, выполняющий нужную работу (перемещающий файл). Сразу после завершения работы вызывается метод, возбуждающий событие.
public void UserInitiatesFileMove()
{
// код здесь перемещает файл.
// после этого мы вызываем метод, возбуждающий событие MoveFile
OnMoveFile();
}
13.2 Код подписчика типового события
Шаг 1: Написание метода обработки события
Этот метод вызывается, когда экземпляр fileMover возбуждает событие MoveFile. Его сигнатура точно совпадает с сигнатурой обработчика события.
void fileMover_MoveFile(object sender, MoveFileEventArgs e)
{
MessageBox.Show(sender.ToString() + " moved the file, " +
e.FileName + ", from " +
e.SourceFolder + " to " + e.DestinationFolder);
}
Шаг 2: Создание экземпляра публикатора события
FileMover fileMover = new FileMover();
Шаг 3: Создание экземпляра обработчика события вместе с регистрацией подписчика (метода обработки события) в событии
Этот подход объединяет шаги 3 и 4 из шагов, перечисленных в разделе 14.2.
fileMover.MoveFile += new FileMover.MoveFileEventHandler(
fileMover_MoveFile);
14. Обработка событий, вызванных компонентами среды разработки .NET – пошаговый разбор и пример
Соглашения по реализации событий, описанные в данной статье, можно обнаружить во всех реализациях событий, принадлежащих среде разработки .NET. Цель данного пошагового руководства – показать, как один компонент среды разработки .NET предоставляет свои события, и как можно написать код, запускающийся при возбуждении события. Вы поймете, что требуемые шаги – подгруппа рекомендуемых шагов, требуемых для реализации ваших собственных событий и методов обработки событий.
Этот раздел показывает, как среда разработки использует эти соглашения путем пошагового разбора реализации события Deleted(удалено) компонента FileSystemWatcher(«сторож файловой системы»). FileSystemWatcher – класс, предоставляемый в пространстве имен System.IO среды разработки .NET. Этот класс может использоваться для уведомления вашего приложения, когда конкретное место ввода/вывода на диск совершается в конкретной папке (например, создан новый файл, файл изменен или удален, и т.д.).
14.1 Реализация события 'Deleted' FileSystemWatcher
Класс System.IO.FileSystemWatcher предоставляет событие Deleted , которое возбуждается экземпляром FileSystemWatcher, когда он обнаруживает, что файл был удален в папке, за которой он следит.
Объявление события
Так класс FileSystemWatcher среды разработки .NET объявляет свое событие Deleted:
public event FileSystemEventHandler Deleted
Объявление делегата
Используется делегат FileSystemEventHandler, объявленный в среде разработки .NET так:
public delegate void FileSystemEventHandler (Object sender,
FileSystemEventArgs e)
Заметьте, что делегат FileSystemEventHandler соответствует соглашению принятия двух параметров – первый, именуемый sender, имеет тип System.Object, и второй параметр, именуемый 'e', имеет тип System.EventArgs или тип его потомка. В этом случае заданный FileSystemEventArgs является потомком (как описано ниже).
Пользовательские EventArgs
Событие Deleted передает информацию об удаленном файле или каталоге с помощью подкласса System.EventArgs:
public class FileSystemEventArgs : EventArgs {}
Класс FileSystemEventArgs расширяет System.EventArgs путем добавления следующих свойств:
• ChangedType - Получает тип произошедшего изменения каталога (передается как значение перечисления WatcherChangeType - создан, удален, изменен, и т.д.)
• FullPath – Получает полный путь к измененному файлу или каталогу
• Name – Получает имя измененного файла или каталога
Когда событие Deleted возбуждается, экземпляр FileSystemWatcher отправляет путь, имя файла и т.д. подписчикам события. Это значит, что каждый подписчик не только узнает, что файл был удален, но и какой именно файл был удален.
Отметьте, что имя обработчика события, FileSystemEventHandler, не полностью соответствует соглашению об именовании, говорящему, что именем обработчика события должно быть имя события, за которым следует слово Handler. Помните, что соглашения – не законы или правила. Наоборот, они являются советами, как сделать ваш код более понятным. В случае класса FileSystemWatcher один обработчик события был реализован для поддержки нескольких событий, включая Deleted, Created и Changed – отсюда и незначительное нарушение строгой интерпретации соглашения об именовании. Строгое следование соглашению привело бы к созданию 3 одинаковых делегатов с разными именами (например, DeletedHandler, CreatedHandler, и т.д.). Или же можно было выбрать имя вроде такого DeletedOrCreatedOrChangedHandler, что было бы нелепо. В данном случае было выбрано разумное отступление от соглашения.
14.2 Обработка события FileSystemWatcher.Deleted
С учетом вышеуказанной реализации события, предоставленной средой разработки .NET, ниже приведен пример кода, который вы могли бы написать в вашем классе, который подписался бы на событие Deleted экземпляра FileSystemWatcher.
Этот код позволил бы вашему приложению реагировать на удаляемые файлы в каталоге C:\Temp.
// Импорт пространства имен
using System.IO;
// Объявление переменной fsWatcher – вероятно, на уровне класса.
FileSystemWatcher fsWatcher = new FileSystemWatcher();
//Инициализация экземпляра – вероятно, в конструкторе или методе, вызванном из
constructor.
fsWatcher.Path = "C:\Temp";
fsWatcher.Deleted += new FileSystemEventHandler(fsWatcher_Deleted);
// это булево свойство включает или отключает возбуждение событий чем-то
FileSystemWatcher instance.
fsWatcher.EnableRaisingEvents = true;
// включите в случае обновления компонентов Windows Forms
fsWatcher.SynchronizingObject = this;
// метод обработки события
void fsWatcher_Deleted(object sender, FileSystemEventArgs e)
{
MessageBox.Show(e.Name + " was deleted" );
}
15. События форм Windows
Формы Windows реализуют свои собственные события, а также обеспечивают обработку событий, возбуждаемых элементами управления, содержащимися внутри формы или внутри контейнерных элементов управления в классе формы. Кроме того, реализация и обработка событий в классах Windows Forms требуют тщательного анализа и даже применения особых шагов с учётом того, что Windows Forms и содержащиеся в них элементы управления проявляют "привязку к потоку" – то есть их свойства может обновлять только код, выполняющийся в том же потоке, который создал форму или элементы управления.
Эти и другие связанные с Windows Forms принципы представлены в данном разделе.
15.1 Различие NET 1.x и 2.0+ - частичные классы
Концепция "частичного класса" была введена в среду разработки .NET версии 2.0. Частичный класс – класс, объявленный с ключевым словом partial, и с частями класса, определенными в двух или более файлах исходного кода. Компилятор извлекает весь исходный код, определяющий частичный класс, из всех файлов, содержащих частичный класс, и выдает один [скомпилированный] класс. То есть частичный класс может располагаться в двух или более файлах исходного кода, но когда приложение компилируется, "части класса" собираются в один класс в выходной сборке.
Преимущества частичных классов включают (1) несколько разработчиков могут работать над разными частями одного и того же класса одновременно, работая с разными файлами исходного кода; и (2) автоматизированные инструменты генерации кода могут писать в один файл исходного кода, в то время как люди-разработчики могут поддерживать свой код в отдельном файле, не беспокоясь, что их изменения впоследствии могут быть переписаны автоматизированным инструментом генерации кода. Это второе преимущество реализовано в проектах Windows Forms, начиная с Visual Studio 2005. Когда вы добавляете новую форму в проект Windows Forms, Visual Studio автоматически создает форму в виде частичного класса, определенного в двух файлах исходного кода. Файл, содержащий код, сгенерированный Visual studio, называется FormName.Designer.cs, тогда как файл, предназначенный для кода разработчика, называется FormName.cs.
Например, если вы заставите Visual Studio 2005 создать форму с именем MainForm, то будут созданы следующие два файла исходного кода:
MainForm.cs – содержит определение частичного класса:
public partial class MainForm : Form
{
// разработчики пишут код здесь
}
MainForm.Designer.cs - содержит определение частичного класса:
partial class MainForm
{
// проектировщик Windows Forms пишет код здесь
}
Когда вы добавляете элементы управления на форму путем использования проектировщика Windows Forms Visual Studio, проектировщик добавляет необходимый код в файл FormName.Designer.cs.
Разработчики не должны непосредственно изменять исходный код в файле FormName.Designer.cs, так как не исключено, что проектировщик перепишет такие изменения. Как правило, весь код разработчика должен быть записан в файле FormName.cs.
.NET 1.x не имеет частичных классов. Весь исходный код – будь то написанный Visual Studio или разработчиком – помещается в один файл исходного кода. Хотя проектировщик Windows Forms Visual Studio стремится писать код только в одном разделе этого файла, можно размещать код разработчика и сгенерированный код в одних и тех же разделах, при наличии вероятности, что проектировщик Windows Forms перепишет код, написанный разработчиком.
15.2 Частичные классы и принципы проектирования Windows Forms для событий
Когда Visual Studio 2005 создает для вас реализацию обработки события, код обработчика события/регистрации записывается в файл FormName.Designer.cs, причем только заглушка метода обработки события автоматически записывается в файл FormName.cs. Цель этой схемы в том, чтобы проектировщик Windows Forms писал весь связанный с событием код, который может быть автоматизирован (подключение метода обработки события к обработчику события и т.д.). Проектировщик не может создавать только специфическую логику программы, которая должна выполняться внутри метода обработки события. Когда Visual Studio заканчивает делать для вас все, что может, вы получаете (1) весь связанный с событием подключающий код, помещенный в файл FormName.Designer.cs; с (2) заглушкой метода обработки события, помещенной в файл FormName.cs. Вам остается только закончить реализацию обработки события, написав требуемый код в заглушке метода обработки события.
15.3 Пошаговый разбор – обработка события Windows Forms
Следующие шаги пошагово разбирают реализацию метода обработки события FormClosing в форме Windows с именем MainForm.
1. С использованием Visual Studio .NET создайте новый проект Windows Forms и добавьте новую форму с именем MainForm.
2. Открыв MainForm в режиме конструктора, щелкните правой кнопкой мыши по открытому участку формы (не по элементу управления), и выберите "Свойства" из всплывающего меню. Появится диалоговое окно "Свойства", отображающее свойства формы или события. Если это еще не выбрано, нажмите кнопку "События" (она имеет иконку светящейся молнии) на панели инструментов вверху диалогового окна свойства.
3. В диалоговом окне события найдите событие, на которое ваше приложение должно реагировать. В нашем случае это событие FormClosing. Дважды щелкните где-нибудь в строке, в которой указано FormClosing.
В этот момент происходят две вещи.
Первое – конструктор Windows Forms вставляет следующую строку в файл MainForm.Designer.cs.
this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(
this.MainForm_FormClosing);
Второе – конструктор Windows Forms вставляет следующую заглушку метода в файл MainForm.cs.
private void MainForm_FormClosing(object sender,
FormClosingEventArgs e)
{
// ваш код обработки события идет здесь
}
* При использовании .NET 1.x (не имеющей частичных классов), генерируется такой же код, но он помещается в один файл MainForm.cs.
Как видно, от вас ничего не скрывают. Весь код, требуемый для реализации логики обработки события, показывается вам и доступен вам для анализа и вероятных изменений при необходимости. Конструктор Windows Forms designer пишет стандартный код, соответствующий рекомендуемым стандартам событий, и помещает этот код в файлы частичного класса, в которых вы затем можете расширять код для связанных с вашим приложением целей.
Если вы хотите изменить имя метода обработки события, сгенерированного для вас конструктором Windows Forms, вы можете сделать это. Обязательно измените имя метода в файле MainForm.cs, и в том месте, где он регистрируется в обработчике события в MainForm.Designer.cs.
Событие FormClosing - это "предшествующее событие", являющееся отменяемым. Это означает, что событие возбуждается перед закрытием формы, и процедура обработки события может отменить событие, тем самым не дав закрыть форму.
В частности, отменяемым это событие делает параметр FormClosingEventArgs, имеющий тип класса, расширяющего System.ComponentModel.CancelEventArgs. Чтобы отменить событие FormClosing, установите булево свойство Cancel FormClosingEventArgs в true так:
private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
{
e.Cancel = true; // предотвращает закрытие формы
}
15.4 Формы Windows и принципы поточной обработки
Свойства и методы форм Windows и содержащихся в них элементов управления могут вызываться только кодом, выполняющимся в том же потоке, который создал элемент управления (т.е. формы Windows и элементы управления проявляют привязку к потоку). Поэтому вы можете столкнуться с неожиданным поведением или исключениями времени выполнения, когда код не из потока пользовательского интерфейса пытается выполнить код, изменяющий компоненты пользовательского интерфейса.
Вы можете столкнуться с проблемами поточной обработки, связанными с этой привязкой к потоку пользовательского интерфейса, даже если вы явно или намеренно не используете несколько потоков в вашем приложении. Например, класс FileSystemWatcher автоматически порождает дополнительный фоновый поток для своих собственных целей.
Следовательно, вы можете столкнуться с проблемами поточной обработки, если ваш код использует FileSystemWatcher, и любые из связанных с ним методов обработки события в конечном счете могут запустить обновление элемента управления пользовательского интерфейса. Любые используемые вами сторонние компоненты могут порождать дополнительные фоновые потоки, о которых вы изначально не знаете.
Есть несколько способов уменьшить эти проблемы поточной обработки:
• Установите свойство SynchronizingObject (при наличии и в соответствующих случаях)
• Используйте Control.InvokeRequired и Control.Invoke() для вызова кода, обновляющего пользовательский интерфейс.
• Разработчики компонентов могут использовать классы SynchronizationContext, AsyncOperation, и AsyncOperationManager.
SynchronizingObject (синхронизирующий объект)
Некоторые компоненты .NET предоставляют свойство SynchronizingObject. Примеры этих компонентов включают классы FileSystemWatcher, Timer и Process. Установка SynchronizingObject позволяет вызывать методы обработки события в том же потоке, который создал компонент пользовательского интерфейса, подлежащий обновлению. Например, событие Elapsed(прошло) таймера возбуждается из потока пула потоков. Когда SynchronizingObject компонент таймера установлен на компонент пользовательского интерфейса, метод обработки события для события Elapsed вызывается в том же потоке, в котором выполняется компонент пользовательского интерфейса. Затем компонент пользовательского интерфейса может быть обновлен из метода обработки события Elapsed.
Нужно отметить, что Visual Studio может автоматически устанавливать свойство SynchronizingObject на элемент управления, содержащий компонент. Следовательно, вы можете вообще не столкнуться с необходимостью явно устанавливать свойство SynchronizingObject.
Однако существуют сценарии, в которых может понадобиться явно установить свойство SynchronizingObject. Например, когда вы имеете библиотеку классов, экземпляр которой создается внутри Windows Form, и эта библиотека классов содержит экземпляр FileSystemWatcher. FileSystemWatcher порождает дополнительный фоновый поток, из которого возбуждаются его события. затем эти события обрабатываются внутри библиотеки классов. Пока все хорошо. Библиотека классов может обрабатывать события, так как у нее отсутствует привязка к потоку, свойственная элементам управления Windows Forms. Библиотека классов может, в ответ на получение события FileSystemWatcher, возбудить новое событие, которое затем обрабатывается в экземпляре, содержащем Windows Form. Возникнет следующее исключение, если SynchronizingObject не был установлен на эту форму (или на соответствующий элемент управления на ней), или код, обновляющий пользовательский интерфейс, не был вызван через Control.Invoke(), как описано далее.
System.InvalidOperationException не было обработано:
Неправильная межпоточная операция: Элемент управления 'ControlNameHere' был вызван из потока, отличного от потока, в котором он был создан.
Control.Invoke() и InvokeRequired
Есть два важных исключения из правила, гласящего, что Элементы управления Windows Forms не могут вызываться из потока, отличного от потока, в котором они были созданы. Все элементы управления наследуют метод Invoke() и свойство InvokeRequired, которые могут быть вызваны из других потоков. Invoke()принимает единственный аргумент, имеющий тип делегата. При вызове Invoke() заставляет делегат вызвать все методы, зарегистрированные в нем. Любой код, вызванный через Invoke(), будет выполнен в том же потоке, в котором находится элемент управления. Чтобы обновить элементы управления пользовательского интерфейса, выполняющиеся в одном потоке, из кода в другом потоке, просто (1) выделите код, обновляющий элемент управления пользовательского интерфейса, в отдельный метод; затем (2) зарегистрируйте этот метод в делегате, который затем (3) передайте в метод Invoke() элемента управления пользовательского интерфейса.
InvokeRequired возвращает истину, когда текущий код выполняется в потоке, отличном от потока, в котором был создан элемент управления. Вы можете запросить значение InvokeRequired, чтобы определить, может ли ваш код напрямую обновить элемент управления, или такие обновления должны передаваться через метод Invoke().
SynchronizationContext, AsyncOperation и AsyncOperationManager
Впервые появившиеся в версии 2.0 среды разработки .NET, эти классы предоставляют разработчикам компонентов, возбуждающим события асинхронно, еще одну возможность решить проблемы поточной обработки, описанные выше. Основное преимущество использования System.ComponentModel.AsyncOperation в том, что он обеспечивает решение проблем поточной обработки (описанных выше) в публикаторе события (компоненте), тогда как две представленных выше альтернативы (Control.Invoke и SynchronizingObject) дают решение для подписчиков.
16. Отменяемые события
Отменяемые события обычно возбуждаются компонентом, собирающимся выполнить некое действие, которое может быть отменено, или совершение которого может быть запрещено. Событие FormClosing класса Windows Form – пример отменяемого события. Типичный сценарий, в котором вам нужно запретить закрытие формы, происходит, когда пользователь не сохранил изменения. В этом сценарии ваш метод обработки события FormClosing может реализовать логику, обнаруживающую наличие несохраненных изменений. Если таковые имеются, то логика может попросить пользователя сохранить свои изменения. Если пользователь выберет сохранить свои изменения, ваша логика отменит событие FormClosing. Это предотвратит закрытие формы, тем самым дав пользователю возможность пересмотреть свои изменения и, вероятно, сохранить их перед повторной попыткой закрыть форму.
Внутренние механизмы отменяемого события могут быть весьма простыми. Учитывая, что события часто сообщают подписчикам, что изменение в состоянии или какое-то другое действие вскоре произойдет, это "предсобытие" дает публикатору события отличную возможность определить, должен ли публикатор позволять изменению в состоянии (или действию) совершиться. Если действию разрешено совершиться (т.е. ничто не говорит публикатору прервать операцию), то публикатор позволяет совершиться изменению в состоянии и впоследствии/дополнительно возбуждает постсобытие.
Итак, отменяемое событие – на самом деле два события и некоторое действие, совершающееся между этими событиями. "Предсобытие" возникает перед действием. После этого действие совершается (или нет). Если действие совершается, обычно возбуждается "постсобытие". Если быть точным, никакое событие не отменяется, несмотря на то, что мы говорим, что имеем отменяемое событие. Наоборот, отменяется действие, совершающееся между двумя событиями, - и, скорее всего, запуск этого действия вообще запрещается.
В поддержку вышеуказанного понятия отменяемого события среда разработки .NET предоставляет класс System.ComponentModel.CancelEventArgs, который можно использовать непосредственно или расширять для связанных с приложением целей. CancelEventArgs расширяет System.EventArgs путем предоставления булева свойства Cancel, которое, если установлено в true подписчиком события, используется публикатором события для отмены события. Код публикатора события создает экземпляр CancelEventArgs, который отправляется подписчикам, когда возбуждается предсобытие. По умолчанию, методы обработки события (в любых/всех подписчиках) запускаются одновременно. Следовательно, изменение состояния (или действие), о котором сообщает предсобытие, может совершиться только после завершения выполнения всех запущенных методов обработки события. Разумеется, публикатор события хранит свою ссылку на экземпляр CancelEventArgs после возбуждения события. Поэтому, если любые методы обработки события установят свойство Cancel в true, публикатор события увидит это до того, как попытается приступить к изменению состояния, и сможет отреагировать соответственно.
Последовательность действий может быть примерно такой:
1. Публикатор события создает экземпляр System.ComponentModel.CancelEventArgs (или его подкласса) с именем 'e'
2. Затем метод возбуждения события возбуждает событие, передавая 'e' подписчикам события (возвращая значение Cancel в false)
3. Затем метод обработки события (в подписчике события) устанавливает значение e.Cancel в true, вероятно, спрашивая у пользователя
4. Затем метод возбуждения события получает значение e.Cancel и реагирует соответственно. В случае если e.Cancel = true, логика не даст совершиться изменению состояния или действию (например, закрытие формы).
В случае если событие имеет несколько подписчиков, событие будет отменено, если любой из методов обработки события установит e.Cancel = true. Точнее, публикатор события увидит, что e.Cancel = true, когда последний метод обработки события возвратит значение (они вызываются одновременно).
В конечном счёте, CancelEventArgs предоставляет подписчику события механизм передачи значения true | false публикатора события. Фактическая работа и смысл "отмены события" - полностью на ваше усмотрение, так как вы должны писать логику, реагирующую на значение e.Cancel.
Отмена долго выполняющейся операции
Описанный выше сценарий отмены события не предлагает никакого механизма для прекращения совершения некоторого действия (или изменения состояния), после того как действие запустилось. Причина в том, что все действия публикации события происходят одновременно (или последовательно). Предсобытие может использоваться для запрета запуска действия, но после того как оно запустилось, оно будет выполняться до завершения, так как методы обработки предсобытия подписчика закончили выполняться, и поэтому больше не могут общаться с публикатором события.
Если вам нужно дать возможность подписчикам отменять операцию после ее запуска (например, после возбуждения и обработки предсобытия), описанного выше базового механизма публикации события будет недостаточно. Вам потребуется использовать более надежный механизм публикации события, в котором публикатор события выполняет свою работу асинхронно (например, в фоновом потоке). Основная идея в том, что клиентский код (в подписчике/наблюдателе) просит, чтобы некая операция совершилось в публикаторе события, затем публикатор события запускает свою операцию в фоновом потоке. Пока фоновая задача выполняется, клиентский код может возобновить другую работу – вероятно, обработку вводимых пользователем графических знаков, – которая может включать в себя запрос на отмену выполняющейся асинхронной операции. Публикатору потребуется реализовать логику, периодически проверяющую поступление запроса на отмену от клиента, и при его наличии прекращающую выполнение своей работы.
Относительно простой и верный способ начать асинхронную обработку – ознакомиться с компонентом System.ComponentModel.BackgroundWorker. Компонент BackgroundWorker позволяет вам запускать задачу асинхронно, сообщать о прогрессе задачи (процент выполнения), отменять задачу после ее запуска, и сообщать о завершении задачи (с помощью возврата значения). Дальнейшее описание моделей асинхронной обработки и вариантов многопоточности и связанных с ними проблем выходит за пределы данной статьи.
17. События веб-форм ASP.NET
Нет ничего сугубо уникального в концепциях, задействованных в создании событий, обработчиков событий, и методов обработки событий в веб-приложениях ASP.NET. Все, что сказано в этой статье о создании пользовательских событий и обработчиков событий, с таким же успехом применяется к веб-приложениям ASP.NET, как и к приложениям Windows Forms и библиотекам кода C#. Веб-приложения ASP.NET радикально отличаются контекстом, в котором события определяются, возбуждаются и обрабатываются. Отсутствие состояний в HTTP и его модель запрос/ответ, роль конвейера запроса HTTP ASP.NET, роль ViewState(состояние просмотра) и т.д. - все вступают в действие – и с осложнениями для возбуждения и обработки событий. За пределами основ событий, изложенных в данной статье, находятся специфичные для ASP.NET и связанные с событиями концепции, такие как обратная передача клиентских событий (написанных на ECMA Script, VBScript или JavaScript) и всплывание события.
Эта статья не берется за рассмотрение событий применительно к веб-приложениям ASP.NET, так как нормальное изложение более чем удвоило бы длину статьи (а эта статья уже достаточно длинная!). Но нужно отметить, что изложенные в данной статье основы дадут начинающему разработчику веб-приложений крепкую базу для развития.