не хватает одного параметра: 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, генерирующем события.
Вот как выглядит теперь интерфейс нашего модифицированного проекта, обеспечивающего взаимодействие, основанное на событиях: