Как обновить поток / класс GUI из рабочего потока / класса? - PullRequest
2 голосов
/ 13 апреля 2010

Первый вопрос здесь, так что всем привет.

Требование, над которым я работаю, - это небольшое тестовое приложение, которое обменивается данными с внешним устройством через последовательный порт. Связь может занять много времени, и устройство может вернуть все виды ошибок.

Устройство приятно абстрагируется в своем собственном классе, который поток GUI начинает запускать в своем собственном потоке и имеет обычные основные функции открытия / закрытия / чтения данных / записи данных. Графический интерфейс также довольно прост - выберите COM-порт, откройте, закройте, покажите чтение данных или ошибки с устройства, разрешите изменение и запись и т. Д.

Вопрос просто в том, как обновить GUI из класса устройства? Существует несколько различных типов данных, с которыми работает устройство, поэтому мне необходим относительно общий мост между классом формы / потока графического интерфейса пользователя и классом / потоком рабочего устройства. В направлении GUI к устройству все отлично работает с [Begin] Invoke вызовами open / close / read / write и т. Д. Для различных событий, генерируемых GUI.

Я прочитал поток здесь (Как обновить GUI из другого потока в C #?) , где делается предположение, что GUI и рабочий поток находятся в одном классе. Поиски в Google показывают, как создать делегата или как создать классического фонового работника, но это совсем не то, что мне нужно, хотя они могут быть частью решения. Итак, есть ли простая, но общая структура, которую можно использовать?

Мой уровень C # умеренный, и я программирую всю свою трудовую жизнь, учитывая подсказку, я выясню это (и отправлю обратно) ... Заранее спасибо за любую помощь.

Ответы [ 3 ]

6 голосов
/ 13 апреля 2010

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

Таким образом, самый простой дизайн тогда будет:

  • добавьте метод в ваш класс пользовательского интерфейса (например, MyUIForm), который называется что-то вроде UpdateUI(), который принимает любую используемую вами структуру данных для передачи данных с устройства в пользовательский интерфейс, который вы используете. Вы можете объявить этот метод в интерфейсе (например, IUIForm), если вы хотите позже поддерживать DI / IoC и иметь форму, реализующую его.
  • в потоке A (потоке пользовательского интерфейса) ваш класс пользовательского интерфейса создает класс устройства, инициализирует все необходимые настройки и запускает его фоновый поток. Он также передает указатель на себя.
  • в потоке B устройство собирает данные и вызывает MyUIForm.UpdateUI() (или IUIForm.UpdateUI()).
  • UpdateUI делает Invoke или BeginInvoke в зависимости от ситуации.

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

Обновление: Для решения ваших проблем с масштабируемостью -

Независимо от того, насколько растет ваше приложение и сколько у вас классов пользовательского интерфейса, вы все равно хотите пересечь границу потока, используя BeginInvoke для конкретного класса пользовательского интерфейса, который вы хотите обновить. (Этот класс пользовательского интерфейса может быть конкретным элементом управления или корнем определенного визуального дерева, это не имеет большого значения). Основная причина в том, что если у вас более одного потока пользовательского интерфейса, вы должны убедиться, что обновление любого пользовательского интерфейса происходит в потоке Этот конкретный пользовательский интерфейс был создан благодаря тому, как работают сообщения Windows и Windows. Следовательно, фактическая логика пересечения граничного потока должна быть заключена в уровне пользовательского интерфейса.

Вашему классу устройства не нужно заботиться о том, какие классы пользовательского интерфейса и в каком потоке необходимо обновить. Фактически, я лично сделал бы устройство полностью неосведомленным о любом пользовательском интерфейсе и просто выставил бы на него события, на которые могут подписаться различные классы пользовательского интерфейса.

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

0 голосов
/ 19 апреля 2010

Итак, после некоторого исследования, основанного на ответах выше, дальнейший поиск в Google и опрос коллег, который немного знаком с C #, выбрал мое решение проблемы ниже. Я по-прежнему заинтересован в комментариях, предложениях и уточнениях.

Сначала некоторые дополнительные подробности о проблеме, которая на самом деле довольно общая в том смысле, что графический интерфейс пользователя контролирует что-то, что должно оставаться полностью абстрактным, через серию событий, на ответы которых графический интерфейс должен реагировать. Есть несколько явных проблем:

  1. Сами события, с разными типами данных. События будут добавляться, удаляться, изменяться по мере развития программы.
  2. Как соединить несколько классов, которые включают графический интерфейс (разные UserControls) и классы, которые абстрагируют аппаратное обеспечение.
  3. Все классы могут генерировать и потреблять события и должны оставаться настолько разъединенными, насколько это возможно.
  4. Компилятор должен выявлять кодовые ошибки как можно дальше (например, событие, которое отправляет один тип данных, но потребитель, ожидающий другой)

Первая часть этого - события. Поскольку GUI и устройство могут вызывать несколько событий, возможно, связанных с ними разных типов данных, диспетчер событий удобен. Это должно быть общим как для событий, так и для данных, поэтому:

    // Define a type independent class to contain event data
    public class EventArgs<T> : EventArgs
    {
    public EventArgs(T value)
    {
        m_value = value;
    }

    private T m_value;

    public T Value
    {
        get { return m_value; }
    }
}

// Create a type independent event handler to maintain a list of events.
public static class EventDispatcher<TEvent> where TEvent : new()
{
    static Dictionary<TEvent, EventHandler> Events = new Dictionary<TEvent, EventHandler>();

    // Add a new event to the list of events.
    static public void CreateEvent(TEvent Event)
    {
        Events.Add(Event, new EventHandler((s, e) => 
        {
            // Insert possible default action here, done every time the event is fired.
        }));
    }

    // Add a subscriber to the given event, the Handler will be called when the event is triggered.
    static public void Subscribe(TEvent Event, EventHandler Handler)
    {
        Events[Event] += Handler;
    }

    // Trigger the event.  Call all handlers of this event.
    static public void Fire(TEvent Event, object sender, EventArgs Data)
    {
        if (Events[Event] != null)
            Events[Event](sender, Data);

    }
}

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

    public enum DEVICE_ACTION_REQUEST
    {
    LoadStuffFromXMLFile,
    StoreStuffToDevice,
    VerifyStuffOnDevice,
    etc
    }

Теперь в любом месте области действия (обычно пространства имен) статического класса EventDispatcher можно определить новый диспетчер:

        public void Initialize()
        {
        foreach (DEVICE_ACTION_REQUEST Action in Enum.GetValues(typeof(DEVICE_ACTION_REQUEST)))
            EventDispatcher<DEVICE_ACTION_REQUEST>.CreateEvent(Action);
        }

Это создает обработчик событий для каждого события в перечислении.

И потребляется подпиской на событие, как этот код в конструкторе объекта-устройства потребления:

        public DeviceController( )
    {
        EventDispatcher<DEVICE_ACTION_REQUEST>.Subscribe(DEVICE_ACTION_REQUEST.LoadAxisDefaults, (s, e) =>
            {
                InControlThread.Invoke(this, () =>
                {
                    ReadConfigXML(s, (EventArgs<string>)e);
                });
            });
    }

Где InControlThread.Invoke - абстрактный класс, который просто переносит вызов invoke.

События могут быть вызваны с помощью графического интерфейса просто:

        private void buttonLoad_Click(object sender, EventArgs e)
        {
            string Filename = @"c:\test.xml";
            EventDispatcher<DEVICE_ACTION_REQUEST>.Fire(DEVICE_ACTION_REQUEST.LoadStuffFromXMLFile, sender, new EventArgs<string>(Filename));
        }

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

Есть много улучшений, которые можно сделать, но это суть проблемы. Мне было бы интересно, как я сказал в комментариях, особенно если есть какие-либо явные упущения / ошибки или недостатки. Надеюсь, это кому-нибудь поможет.

0 голосов
/ 13 апреля 2010

Это версия с обработчиком событий.
Он упрощен, поэтому в форме отсутствуют элементы управления пользовательским интерфейсом и отсутствуют свойства в классе SerialIoEventArgs.

  1. Поместите свой код для обновления пользовательского интерфейса под комментарием // Обновить пользовательский интерфейс
  2. Поместите код для чтения последовательного ввода-вывода под комментарием // Чтение из последовательного ввода-вывода
  3. Добавьте поля / свойства в класс SerialIoEventArgs и заполните его методом OnReadCompleated.
public class SerialIoForm : Form
{
    private delegate void SerialIoResultHandlerDelegate(object sender, SerialIoEventArgs args);
    private readonly SerialIoReader _serialIoReader;
    private readonly SerialIoResultHandlerDelegate _serialIoResultHandler;

    public SerialIoForm()
    {
        Load += SerialIoForm_Load;
        _serialIoReader = new SerialIoReader();
        _serialIoReader.ReadCompleated += SerialIoResultHandler;
        _serialIoResultHandler = SerialIoResultHandler;
    }

    private void SerialIoForm_Load(object sender, EventArgs e)
    {
        _serialIoReader.StartReading();
    }
    private void SerialIoResultHandler(object sender, SerialIoEventArgs args)
    {
        if (InvokeRequired)
        {
            Invoke(_serialIoResultHandler, sender, args);
            return;
        }
        // Update UI
    }
}
public class SerialIoReader
{
    public EventHandler ReadCompleated;
    public void StartReading()
    {
        ThreadPool.QueueUserWorkItem(ReadWorker); 
    }
    public void ReadWorker(object obj)
    {
        // Read from serial IO

        OnReadCompleated();
    }

    private void OnReadCompleated()
    {
        var readCompleated = ReadCompleated;
        if (readCompleated == null) return;
        readCompleated(this, new SerialIoEventArgs());
    }
}

public class SerialIoEventArgs : EventArgs
{
}
...