如何从工作线程/类更新GUI线程/类?

时间:2010-04-13 07:03:29

标签: c# multithreading user-interface worker

这里的第一个问题让大家好。

我正在处理的要求是一个小型测试应用程序,它通过串行端口与外部设备进行通信。通信可能需要很长时间,设备可以返回各种错误。

该设备在其自己的类中很好地抽象,GUI线程开始在其自己的线程中运行,并具有通常的打开/关闭/读取数据/写入数据基本功能。 GUI也很简单 - 选择COM端口,打开,关闭,显示数据读取或设备错误,允许修改和回写等。

问题是如何从设备类更新GUI?设备处理的数据有几种不同类型,因此我需要在GUI表单/线程类和工作设备类/线程之间建立一个相对通用的桥梁。在GUI到设备方向上,一切正常,在各种GUI生成的事件中,[Begin] Invoke调用open / close / read / write等。

我已经阅读了线程here (How to update GUI from another thread in C#?),其中假设GUI和工作线程在同一个类中。 Google搜索引发了如何创建委托或如何创建经典后台工作者,但这根本不是我需要的,尽管它们可能是解决方案的一部分。那么,是否有一个可以使用的简单但通用的结构?

我的C#水平是温和的,我一直在编程我的所有工作生活,给出一个线索我会弄清楚(并回复)...提前感谢任何帮助。

3 个答案:

答案 0 :(得分:6)

您可以在UI类上公开设备类可以在后台线程上调用的公共方法,其中包含传递给UI所需的所有信息。该公共方法将在后台线程的上下文中执行,但由于它属于UI类,因此您现在可以使用您已阅读的任何调用编组技术。

因此,最简单的设计是:

  • 向您的UI类添加一个方法(例如MyUIForm),类似于UpdateUI(),它采用您使用的任何数据结构将数据从设备传递到您使用的UI。您可以在接口中声明该方法(例如IUIForm),如果您想稍后支持DI / IoC,并让表单实现它。
  • 在线程A(UI线程)上,您的UI类创建设备类,初始化所有必要的设置并启动其后台线程。它还传递一个指向自身的指针。
  • 在主题B上,设备会收集数据并调用MyUIForm.UpdateUI()(或IUIForm.UpdateUI())。
  • UpdateUI视情况InvokeBeginInvoke

请注意,在UI类中封装所有UI和表示逻辑具有附带的好处。您的设备类现在可以专注于处理硬件。

更新:解决您的可扩展性问题 -

无论您的应用程序增长多少以及您拥有多少UI类,您仍然希望使用BeginInvoke跨越线程边界来处理您要更新的特定UI类。 (那个UI类可能是一个特定的控件或特定可视化树的根,它并不重要)主要原因是如果你有多个UI线程,你必须确保在线程上发生任何UI的更新由于Windows消息传递和Windows的工作方式,因此创建了此特定用户界面。因此,跨越边界线程的实际逻辑应该封装在UI层中。

您的设备类不应该关心哪些UI类以及需要更新的线程。事实上,我个人会让设备完全无视任何UI,并且只会在其上公开不同UI类可以订阅的事件。

请注意,替代解决方案是使线程完全封装在设备类中,并使UI不知道是否存在bacground线程。然而,然后线程边界交叉成为设备类的责任并且应该包含在其逻辑中,因此您不应该使用跨越线程的UI方式。这也意味着您的设备类绑定到特定的UI线程。

答案 1 :(得分:0)

这是一个带有事件处理程序的版本 它被简化,因此表单中没有UI控件,而且SerialIoEventArgs类中没有属性。

  1. 放置代码以更新注释//更新用户界面
  2. 下的用户界面
  3. 将代码放在注释//读取串行IO左
  4. 下的串行IO中
  5. 将字段/属性添加到SerialIoEventArgs类并在方法OnReadCompleated中填充它。
  6. 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
    {
    }
    

答案 2 :(得分:0)

因此,经过一些基于上述答案的研究,进一步谷歌搜索并询问了解C#的同事,我选择的问题解决方案如下。我仍然对评论,建议和改进感兴趣。

关于这个问题的第一个更详细的细节,实际上是非常通用的,因为GUI控制某些东西,必须保持完全抽象,通过一系列事件,GUI必须作出反应。有一些不同的问题:

  1. 事件本身,具有不同的数据类型。随着程序的发展,事件将被添加,删除和更改。
  2. 如何桥接构成GUI的几个类(不同的UserControls)和抽象硬件的类。
  3. 所有类都可以生成和使用事件,并且必须尽可能保持解耦。
  4. 编译器应该尽可能地发现编码cockups(例如,发送一种数据类型的事件,但是需要另一种数据类型的消费者)
  5. 第一部分是事件。由于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);
    
        }
    }
    

    现在我们需要一些来自C世界的事件,我喜欢枚举,所以我定义了一些GUI会引发的事件:

        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);
            }
    

    这将为枚举中的每个事件创建一个事件处理程序。

    通过在消费Device对象的构造函数中订阅此代码之类的事件来消费:

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

    InControlThread.Invoke是一个简单包装调用调用的抽象类。

    GUI可以简单地引发事件:

            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),编译器会发牢骚。

    可以进行许多增强,但这是问题的关键。正如我在评论中所说,我会感兴趣,特别是如果有任何明显的遗漏/错误或缺陷。希望这有助于某人。