|
не хватает одного параметра: static void Main(string[] args) |
Интерфейс и многопоточность
Три способа взаимодействия управляющего и управляемого процессов
Взаимодействие между управляющим процессом и управляемым можно организовать разными способами. Рассмотрим три основных способа:
- Взаимодействие построено на основе механизма ссылок. Класс F, описывающий управляемый процесс, содержит ссылку на управляющий объект - объект класса G. В свою очередь управляющий класс G содержит ссылку на управляемый объект - объект класса F.
- Взаимодействие построено на основе механизма генерирования событий. Управляющий объект класса F генерирует событие, а управляемый объект его обрабатывает. В свою очередь управляемый объект класса G генерирует событие, а управляющий объект его обрабатывает.
- Взаимодействие построено на основе обработки событий таймера. Управляющий процесс в определенные интервалы времени передает и получает информацию от управляемого им процесса.
Что происходит, когда управляющий и управляемый процесс находятся в разных потоках? Все три способа взаимодействия осуществимы и в этом случае. Но когда управляющий процесс представлен интерфейсным классом, то возникает особенность, уже рассмотренная нами. Другим потокам запрещается напрямую обращаться к элементам управления интерфейсного класса, работающего в другом потоке.
Взаимодействие, построенное на основе механизма ссылок, управляющего интерфейсного класса и управляемого класса, реализующего бизнес - логику, подробно рассмотрено на примере проектов CorrectExample и WindowsInterfaceAndThreads. Справиться с проблемой доступа к элементам интерфейса из другого потока позволяет вызов метода Invoke, которым обладают все элементы интерфейса.
Недостаток схемы взаимодействия на основе ссылок состоит в том, что не только интерфейсный класс содержит ссылку на класс, реализующий бизнес - логику, но и класс бизнес - логики должен содержать ссылку на интерфейсный класс. Это противоречит правилам хорошего программирования, - бизнес - логика не должна привязываться к фиксированному интерфейсу. Схема, построенная на событиях, где объект, зажигающий событие, не знает, какие объекты и каких классов будут обрабатывать это событие, представляется предпочтительней. Но возможно ли зажигать событие в одном потоке, а обрабатывать его в другом потоке? Давайте рассмотрим подробнее эту схему.
Взаимодействие, основанное на событиях
Рассмотрим пример взаимодействия интерфейсного класса и класса бизнес - логики, основанное на событиях. Наша цель состоит в том, чтобы класс бизнес логики не содержал ссылку на интерфейсный класс. Интерфейсный класс, являющийся управляющим классом, конечно же, содержит ссылки на объекты, которыми он управляет. В тот момент, когда объект бизнес - логики вырабатывает новое значение параметра, принадлежащего к наблюдаемым параметрам, он будет, зажигая соответствующее событие, передавать это параметр всем объектам, принимающим событие. В интерфейсном классе, подписанном на получение сообщения о событии, событие может быть должным образом обработано.
Модифицируем наш последний пример. По аналогии с проектом WindowsInterfaceAndThreads построим проект WindowsFormsInterfaceAndEvents. Введем в рассмотрение класс Worker_Ev, который в отличие от ранее рассмотренного класса Worker, не будет содержать ссылку на интерфейсный класс, но будет включать событие, позволяющее уведомить интерфейсный класс о выработке новых значений наблюдаемых параметров.
Напомню, общую схему построения класса с событиями. Первым делом нужно объявить делегат, описывающий сигнатуру события. Принято, чтобы сигнатура события задавалась двумя параметрами, первый из которых задает объект, возбуждающий событие, а второй параметр представлял объект некоторого класса, наследуемого от класса EventArgs, содержащего параметры, передаваемые обработчикам события. В класс, вырабатывающий событие, нужно добавить объявление события с соответствующей сигнатурой, метод OnFire, зажигающий событие, и в нужных местах, где событие возникает, вызывать этот метод. В классах, которые хотят подписаться на получение сообщения о событии, следует подключить к событию обработчик события, имеющий соответствующую событию сигнатуру. Подробнее об этом можно прочесть в [9].
Вот как выглядит описание делегата, задающего сигнатуру события:
public delegate void ResultEventHandler(object sender, ResultEventArgs args);
Класс ResultEventArgs, описывающий параметры, передаваемые обработчикам события, выглядит так:
public class ResultEventArgs : EventArgs
{
/// <summary>
/// наблюдаемые параметры представляют
/// входные аргументы, передаваемые обработчику события
/// </summary>
string val, quality;
public string Val
{
get { return val; }
}
public string Quality
{
get { return quality; }
}
public ResultEventArgs(string val, string quality)
{
this.val = val;
this.quality = quality;
}
}Класс Worker_Ev не содержит ссылки на интерфейсный класс, но содержит поле, задающее событие:
public event ResultEventHandler result_event;
Метод этого класса, которому обычно дается имя OnFire имеет стандартный вид:
/// <summary>
/// "Зажигает" события
/// </summary>
/// <param name="args">аргументы, передаваемые обработчику</param>
public void OnFire(ResultEventArgs args)
{
if(result_event != null)
result_event(this, args);
}Метод, моделирующий процесс бизнес-логики, достаточно прост:
/// <summary>
/// Метод, осуществляющий процесс работы
/// </summary>
public void Run()
{
while (!stop)
{
SetVisionParams();
}
}Переменная stop - это управляемая переменная. Когда в интерфейсном классе будет нажата соответствующая кнопка, выполнение цикла в Run завершится. Метод SetVisionParams вырабатывает значения наблюдаемых параметров, которые должны передаваться управляющему процессу через механизм событий. Вот текст этого метода:
/// <summary>
/// Создает значения наблюдаемых параметров
/// Зажигает событие
/// </summary>
public void SetVisionParams()
{
int val_i;
val_i = rnd.Next(minLimit, maxLimit + 1);
if (val_i == minLimit)
quality = EQuality.Ниже_Нормы.ToString();
else if (val_i == maxLimit)
quality = EQuality.Выше_нормы.ToString();
else
quality = EQuality.Норма.ToString();
numer++;
val = numer + "." + val_i;
quality = numer + "." + quality;
//зажигаем событие - получены результаты
OnFire(new ResultEventArgs(val, quality));
}Интерфейсный класс, поле которого содержит ссылку worker, при запуске процесса бизнес - логики, создает этот объект и присоединяет к нему обработчик этого события:
private void buttonStart_Click(object sender, EventArgs e)
{
buttonClear.Enabled = false;
textBoxMessage.Text = "";
// Создаем объект бизнес-логики
worker = new Worker_Ev();
//Присоединяем обработчик события объекта worker
worker.result_event += new ResultEventHandler(SetVisions_Event);
//Устанавливаем границы
GetLimits();
//Создаем дочерний поток для выполения процесса бизнес-логики
Thread workerThread = new Thread(worker.Run);
//запускаем процесс
workerThread.Start();
}Когда при работе процесса бизнес - логики в другом потоке, вырабатываются значения наблюдаемых параметров и зажигается событие, то в интерфейсном классе, получившем сообщение о событии, вызывается обработчик события:
/// <summary>
/// запись наблюдаемых параметров
/// в интерфейсные элементы
/// </summary>
public void SetVisions_Event(object sender, ResultEventArgs args)
{
listBoxValues.Items.Add(args.Val);
listBoxValues.Refresh();
listBoxQuality.Items.Add(args.Quality);
listBoxQuality.Refresh();
}В данном обработчике события запись наблюдаемых параметров производится непосредственно в элементы интерфейса - элементы listBox. Все хорошо работает, если запускать Release версию приложения (Ctrl + F5). Но в Debug версии, при работе в отладочном режиме возникает исключительная ситуация, показанная на следующем рисунке:
Когда в приложении есть только два потока - один для интерфейса, другой для бизнес - логики, то работать без вызова метода Invoke безопасно. Но, конечно же, возникновение исключительной ситуации для отладочной версии не годится для профессиональной разработки. Необходимо найти другой способ работы в схеме событий.
Решается эта проблема достаточно просто. Записывать данные в элементы интерфейсного класса из другого потока не разрешается, но в поля этого класса запись разрешена. Поэтому вполне возможно создать в интерфейсном классе контейнер, куда будут записываться значения наблюдаемых параметров. Достаточно теперь в интерфейсном классе иметь командную кнопку, по нажатии которой на законных основаниях данные из контейнера будут переноситься в элементы интерфейса. Реализуем эту стратегию.
Обработчик события SetVisionsEvent, который приводил к исключительной ситуации, запишем теперь в следующем виде:
/// <summary>
/// запись наблюдаемых параметров
/// в контейнеры (списки)
/// </summary>
public void SetVisions_Event(object sender, ResultEventArgs args)
{
list_val.Add(args.Val);
list_quality.Add(args.Quality);
}Здесь list_val и list_quality - два контейнера - два списка, в которые обработчик события помещает наблюдаемые параметры. В интерфейс проекта добавлена командная кнопка, позволяющая в нужный момент переносить данные из контейнеров в соответствующие элементы интерфейса, - списки, отображаемые на экране:
/// <summary>
/// перенос данных из контейнеров
/// в отображаемые элементы интерфейса
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void buttonDataSet_Click(object sender, EventArgs e)
{
for (int i = 0; i < list_val.Count; i++)
{
listBoxValues.Items.Add(list_val[i]);
listBoxQuality.Items.Add(list_quality[i]);
}
}Заметьте, все эти изменения в интерфейсном классе, никак не отразились на классе Worker_Ev, генерирующем события.
Вот как выглядит теперь интерфейс нашего модифицированного проекта, обеспечивающего взаимодействие, основанное на событиях:

