• Microsoft .NET
  • C#.NET
  • Основы, лучшие методы и соглашения реализации событий в C#

Основы, лучшие методы и соглашения реализации событий в C# - События форм Windows, отменяемые события и события веб-форм ASP.NET

ОГЛАВЛЕНИЕ

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, так как нормальное изложение более чем удвоило бы длину статьи (а эта статья уже достаточно длинная!). Но нужно отметить, что изложенные в данной статье основы дадут начинающему разработчику веб-приложений крепкую базу для развития.