Опубликован: 23.04.2013 | Доступ: свободный | Студентов: 856 / 185 | Длительность: 12:54:00
Лекция 9:

Интерфейс и многопоточность

< Лекция 8 || Лекция 9: 12345 || Лекция 10 >

Пример построения интерфейса с простым управлением

Рассмотрим теперь пример простого приложения, демонстрирующий корректную работу интерфейса, позволяющего управлять процессом вычислений. На следующем рисунке показан интерфейс нашего приложения:

Интерфейс приложения CorrectExample

Рис. 8.1. Интерфейс приложения CorrectExample

Интерфейс построен в соответствии с описанной схемой. В контейнере "Наблюдение" размещен элемент ListBox, в котором по ходу вычислений будут отображаться значения вычисляемого параметра. В контейнере "Управление" находятся три командные кнопки. Кнопка "Пуск" позволяет запускать процесс вычислений. Кнопка "Стоп" позволяет в любой момент прервать вычисления. Кнопка "Очистить" позволяет очистить список наблюдаемых значений параметра. Рисунок показывает состояние проекта в момент вычислений. Можно заметить, что в этот момент кнопка "Очистить" отключена. Она включается только после того, как вычисления остановлены по нажатию кнопки "Стоп". При запуске процесса вычислений она снова переходит в режим "Выключена". Это гарантирует, что не возникнет ситуация, при которой два потока будут пытаться одновременно писать в список и пытаться его очистить.

Перейдем к описанию проекта. Начнем с бизнес-логики. Как и положено, бизнес-логика отделена от интерфейса и находится в отдельном классе, названном Worker. (Я не стал создавать DLL, чтобы не загромождать изложение деталями.)

Этот класс содержит, во-первых, ссылку на интерфейсный класс, необходимую для обеспечения взаимодействия. Во-вторых, в классе должны быть наблюдаемые и управляющие параметры, значениями которых класс Worker будет обмениваться с интерфейсным классом. Ну и наконец, в классе должен быть метод, реализующий соответствующий бизнес-процесс. В нашей модели все устроено достаточно просто.

namespace WindowsFormsCorrectExample
{
    /// <summary>
    /// Класс, реализующий бизнес-логику проекта
    /// </summary>
    class Worker
    {
        //Ссылка на интерфейсный класс
        FormCorrect correctForm;

        //Управляющая переменная, изменяемая в интерфейсе
        bool stop = false;
          
        Random rnd = new Random();
        
        /// <summary>
        /// Конструктор
        /// </summary>
        /// <param name="form">ссылка на интерфейсный класс</param>
        public Worker(FormCorrect form)
        {
            correctForm = form;
        }
        
        /// <summary>
        /// Доступ на запись управляющей переменной
        /// </summary>
        public bool Stop
        {
            set { stop = value; }
        }
        /// <summary>
        /// Основной метод, реализующий логику проекта
        /// В цикле моделируется значений наблюдаемого параметра,
        /// передаваемого в список, отображаемый в интерфейсе проекта
        /// Завершение цикла вычислений зависит от пользователя
        /// и происходит при нажатии кнопки "Стоп"
        /// </summary>
        public void Run()
        {
            string res = "";
            int i = 1;
            while (!stop)
            {
                //Вычисление наблюдаемого параметра
                res = i.ToString() + "." + rnd.Next(100).ToString();
                i++;
               //Наблюдаемый параметр передается основному потоку
              correctForm.Invoke(correctForm.myDelegate, new object[] {res});               
            }
        }
    }
}

Класс снабжен комментариями, которых, надеюсь, с учетом ранее сделанных замечаний достаточно для понимания его работы. Главное, на что следует обратить внимание, это вызов метода Invoke для передачи очередного значения наблюдаемого параметра res для отображения в интерфейсе.

Рассмотрим теперь организацию интерфейсного класса. Деталей здесь больше. Начнем с обработчика центральной кнопки "Пуск", запускающей вычисления:

/// <summary>
        /// Запуск вычислений в новом потоке
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void buttonStart_Click(object sender, EventArgs e)
        {
            buttonClear.Enabled = false;
            Thread myThread = new Thread(Pusk);
            myThread.Start();
        }

Здесь создается новый поток и ему передается метод класса, выполняемый в этом потоке, - метод Pusk, не имеющий аргументов. Этот метод устроен также просто:

/// <summary>
        /// Метод, выполняемый в потоке
        /// Вызывает метод класса Worker, реализующий бизнес-логику
        /// </summary>
        void Pusk()
        {
            worker = new Worker(this);
            worker.Run();
        }

В задачу этого метода входит создание экземпляра класса Worker и вызов метода Run, выполняющего вычисления.

Завершение вычислений инициируется пользователем в тот момент, когда он нажимает на кнопку "Стоп". Обработчик этого события соответствующее управляющее воздействие передает объекту worker, изменяя значение управляющей переменной stop с false на true.

/// <summary>
        /// управление вычислениями
        /// Значение управляющей переменной передается объекту worker
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void buttonStop_Click(object sender, EventArgs e)
        {
            worker.Stop = true;
            buttonClear.Enabled = true; 
        }

А теперь приведу текст класса в целом, опуская ранее приведенные фрагменты кода:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Threading;

namespace WindowsFormsCorrectExample
{
    public delegate void Delegate_Void_String(string par); 
    public partial class FormCorrect : Form
    {
       public Delegate_Void_String myDelegate;
       Worker worker; 
        
        public FormCorrect()
        {
            InitializeComponent();
            myDelegate = AddList;
        }
        
        /// <summary>…        
        private void buttonStart_Click(object sender, EventArgs e)…
        
/// <summary>…        
        void Pusk()…
        
        /// <summary>…        
        void AddList(string item)…
        
        /// <summary>…        
        private void buttonStop_Click(object sender, EventArgs e)…
        

        private void buttonClear_Click(object sender, EventArgs e)
        {
            listBoxValues.Items.Clear();
        }
    }
}

Прежде чем закончить рассмотрение этого простого примера, сделаю одно важное замечание, поясняющее, как нужно понимать слова о том, что поток не может непосредственно обращаться к элементам интерфейса, созданным в другом потоке. Давайте посмотрим, что произойдет, если вместо вызова метода Invoke, упаковывающего обращение к методу, получающему доступ к элементу управления, будем непосредственно вызывать нужный нам метод. Заменим вызов:

correctForm.Invoke(correctForm.myDelegate, new object[] { res });

на вызов:

correctForm.myDelegate(res);

Если запустить проект в отладочном режиме, то умный отладчик обнаружит некорректное обращение к элементу управления из другого потока и выбросит исключение с соответствующим уведомлением. Если же запустить проект в исполняемом режиме (Ctrl + F5), то исключительная ситуация не возникает.

Пример интерфейса с управляющими переменными

Давайте чуть усложним интерфейс проекта, добавив управляющие переменные, значения которых пользователь может задавать, не останавливая процесс вычислений, но воздействуя на получаемые результаты. Построим новый проект WindowsInterfaceAnd Threads, представляющий расширенный вариант проекта CorrectExample. Этот проект будет также содержать класс Worker, реализующий бизнес - логику, и интерфейсный класс. На следующем рисунке показан новый расширенный вариант интерфейса:

Расширенный вариант интерфейса проекта с управляющими переменными

увеличить изображение
Рис. 8.2. Расширенный вариант интерфейса проекта с управляющими переменными

В контейнере "Наблюдение" будут отображаться значения двух наблюдаемых параметров". В контейнере "Управление" появилась дополнительная кнопка "Изменить пределы и два текстовых окошка для задания значений управляющих переменных, названных нижним и верхним пределом. Пользователь в любой момент может изменить значения этих переменных, но вступят они в силу только после нажатия соответствующей кнопки. Было бы неразумно, если бы поток, осуществляющий вычисления, получал доступ к этим переменным параллельно с изменением их значений. Поэтому в таких ситуациях следует отделять изменение значений пользователем от момента вступления в силу измененных значений.

Данный проект во многом схож с ранее описанным проектом CorrectExample. Здесь также действуют два класса - интерфейсный класс, названный FormSeeAndControl, и класс Worker, реализующий бизнес-логику. По этой причине проект подробно описывать не буду, оставляя его полную реализацию читателям. Остановлюсь лишь на некоторых деталях. Поскольку значения управляющих параметров задаются пользователем, то ввод необходимо контролировать (значения должны быть целыми числами, в определенном диапазоне, нижний предел должен быть меньше верхнего и так далее). Что делать, если условия нарушаются? Можно было бы вместе с выдачей соответствующего сообщения (окно для выдачи сообщения предусмотрено) останавливать процесс, ожидая исправления данных. В данном варианте вычисления продолжаются со значениями, принятыми по умолчанию.

На одном отличии этого проекта от предыдущего остановлюсь подробнее, поскольку оно важно для разработки любых проектов с управляющим интерфейсом. В предыдущем проекте поток, которому требовалось передать данные элементу интерфейса, в методе Invoke задавал два аргумента - метод и данные. Это требовало задания специального делегата, описывающего сигнатуру метода. Можно поступать по-другому, используя технику, типичную для объектно-ориентированного программирования. Можно иметь в классе соответствующие поля, методы берут значения из этих полей, а потому не имеют никаких аргументов - они работают только с полями класса. В этом случае все вызовы Invoke можно свести к универсальной стандартной схеме - вызову метода без аргументов, сигнатура которого удовлетворяет стандартному типу - MethodInvoker. В данном проекте все так и делается. Для управляющих переменных в интерфейсном классе введены поля:

int lowLimit, highLimit;

В классе Worker у них другие имена:

int minLimit= 0, maxLimit = 10;

Поля для наблюдаемых переменных в классе Worker имеют имена:

string val = "";
string quality ="";

Поля закрыты, но заданы открытые свойства, позволяющие клиентам класса получать доступ к полям.

Метод Run теперь выглядит так:

/// <summary>
        /// Метод, осуществляющий процесс работы
        /// </summary>
        public void Run()
        {
            GetControlParams();
            while (!stop)
            {
                if (change)
                {
                    GetControlParams();
                    change = false;
                }
                SetVisionParams();
            }
        }

По ходу работы метод Run вызывает два метода - один для получения значений управляющих параметров, другой для передачи значений наблюдаемых параметров. Для управляемых переменных вначале вызывается через Invoke метод, читающий их значения в поля интерфейсного класса. Потом эти значения переписываются в поля класса Worker:

/// <summary>
        /// Читает значения управляющих параметров
        /// </summary>
        void GetControlParams()
        {
            myForm.Invoke(new MethodInvoker(myForm.GetLimits));
            minLimit = myForm.LowLimit;
            maxLimit = myForm.HighLimit;
        }

Наблюдаемые значения создаются, их значения записываются в поля класса, а затем через Invoke вызывается метод интерфейсного класса, отображающий значения, хранимые в полях, в соответствующих элементах интерфейса:

/// <summary>
        /// Пишет значения наблюдаемых параметров
        /// </summary>
        public void SetVisionParams()
        {
            int vali = rnd.Next(minLimit, maxLimit + 1);
            if (vali == minLimit)
                quality = EQuality.Ниже_Нормы.ToString();
            else if (vali == maxLimit)
                quality = EQuality.Выше_нормы.ToString();
            else
                quality = EQuality.Норма.ToString();
            numer++;
            val = numer + "." + vali;
            quality = numer + "." + quality;
            myForm.Invoke(new MethodInvoker(myForm.SetVisions));
        }

Методы интерфейсного класса GetLimits и SetVisions описывать не буду, оставляя их для самостоятельного рассмотрения. Еще раз обращаю внимание на то, что переход к использованию класса MethodInvoker делает схему работы не только универсальной, но и более эффективной по времени реализации, так что следует пользоваться полями класса для передачи информации.

< Лекция 8 || Лекция 9: 12345 || Лекция 10 >
Алексей Рыжков
Алексей Рыжков

не хватает одного параметра:

static void Main(string[] args)
        {
            x = new int[n];
            Print(Sample1,"original");
            Print(Sample1P, "paralel");
            Console.Read();
        }

Никита Белов
Никита Белов

Выставил оценки курса и заданий, начал писать замечания. После нажатия кнопки "Enter" окно отзыва пропало, открыть его снова не могу. Кнопка "Удалить комментарий" в разделе "Мнения" не работает. Как мне отредактировать недописанный отзыв?