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

发布于 2024-08-28 06:49:39 字数 662 浏览 6 评论 0原文

第一个问题就到这里,大家好。

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

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

问题只是如何从设备类更新 GUI?设备处理多种不同类型的数据,因此我需要在 GUI 表单/线程类和工作设备类/线程之间建立一个相对通用的桥梁。在 GUI 到设备的方向上,一切都可以通过 [Begin]Invoke 调用在各种 GUI 生成的事件上进行打开/关闭/读/写等操作正常进行。

我已阅读线程 此处(如何从另一个更新 GUI C# 中的线程?) 其中假设 GUI 和工作线程位于同一类中。谷歌搜索提出了如何创建委托或如何创建经典的后台工作人员,但这根本不是我需要的,尽管它们可能是解决方案的一部分。那么,有没有一个简单但通用的结构可以使用呢?

我的 C# 水平中等,我的整个工作生涯都在编程,如果有线索我会弄清楚(并回发)...提前感谢您的帮助。

First question here so hello everyone.

The requirement I'm working on is a small test application that communicates with an external device over a serial port. The communication can take a long time, and the device can return all sorts of errors.

The device is nicely abstracted in its own class that the GUI thread starts to run in its own thread and has the usual open/close/read data/write data basic functions. The GUI is also pretty simple - choose COM port, open, close, show data read or errors from device, allow modification and write back etc.

The question is simply how to update the GUI from the device class? There are several distinct types of data the device deals with so I need a relatively generic bridge between the GUI form/thread class and the working device class/thread. In the GUI to device direction everything works fine with [Begin]Invoke calls for open/close/read/write etc. on various GUI generated events.

I've read the thread here (How to update GUI from another thread in C#?) where the assumption is made that the GUI and worker thread are in the same class. Google searches throw up how to create a delegate or how to create the classic background worker but that's not at all what I need, although they may be part of the solution. So, is there a simple but generic structure that can be used?

My level of C# is moderate and I've been programming all my working life, given a clue I'll figure it out (and post back)... Thanks in advance for any help.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

朕就是辣么酷 2024-09-04 06:49:39

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

因此,最简单的设计是:

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

请注意,这样做的另一个好处是将所有 UI 和表示逻辑封装在 UI 类中。您的设备类现在可以专注于处理硬件。

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

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

您的设备类不必关心哪些 UI 类以及哪个线程需要更新。事实上,我个人会让设备完全不了解任何 UI,而只是公开不同 UI 类可以订阅的事件。

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

You can expose a public method on your UI class that the device class can call on the background thread with all the information it needs to pass to the UI. That public method will be executed in the context of the background thread, but since it belongs to the UI class, you can now employ any of the call marshaling techniques you've read about.

Thus, the simplest design then would be:

  • add a method to your UI class (for example MyUIForm)called something like UpdateUI() that takes whatever data structure you are using to pass the data from the device to the UI you use. You can declare that method in an interface (for example IUIForm), if you want to support DI/IoC later, and have the form implement it.
  • on thread A (the UI thread), your UI class creates the device class, initializes all the necessary settings and starts its background thread. It also passes a pointer to itself.
  • on thread B, the device collects the data and calls MyUIForm.UpdateUI() (or IUIForm.UpdateUI()).
  • UpdateUI does Invoke or BeginInvoke as appropriate.

Note that has the side benefit of encapsulating all the UI and presentation logic in your UI class. Your device class can now focus on dealing with the hardware.

Update: To address your scalability concerns -

No matter how much your app grows and how many UI classes you have, you still want to cross the thread boundary using the BeginInvoke for the particular UI class you want to update. (That UI class might be a specific control or the root of a particular visual tree, it does not really matter) The main reason is if you have more than one UI threads, you have to ensure the update of any UI happens on the thread this particualr UI was created on, due to the way Windows messaging and windows work. Hence, the actual logic of crossing the boundary thread should be encapsulated in the UI layer.

You device class should not have to care what UI classes and on which thread need to be updated. In fact, I personally would make the device fully ignorant about any UI and would just expose events on it that different UI classes can subscribe on.

Note that the alternative solution is to make the threading fully encapsulated in the device class and make the UI ignorant about the existence of a bacground thread. However, then thread boundary crossing then becomes responsibility of the device class and should be contained within its logic, so you shouldn't be using the UI way of crossing the threads. That also means your device class is bound to particular UI thread.

对你的占有欲 2024-09-04 06:49:39

这是带有事件处理程序的版本。
它经过简化,因此表单中没有 UI 控件,SerialIoEventArgs 类中也没有属性。

  1. 将用于更新 UI 的代码放在注释下 // Update UI
  2. 将用于读取串行 IO 的代码放在注释下// 从串行 IO 读取
  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
{
}

This is a version with an event handler.
It is simplified so there is no UI controls in the form and no properties in the SerialIoEventArgs class.

  1. Place your code to update the UI under the comment // Update UI
  2. Place your code to read serial IO under the comment // Read from serial IO
  3. Add fields/properties to SerialIoEventArgs class and populate it in method 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
{
}
只是偏爱你 2024-09-04 06:49:39

因此,根据上述答案进行一些研究后,进一步 Google 搜索并询问一位对 C# 了解一点的同事,我选择的问题解决方案如下。我仍然对评论、建议和改进感兴趣。

首先是关于这个问题的一些进一步的细节,这实际上是相当通用的,因为 GUI 正在控制一些东西,这些东西必须保持完全抽象,通过 GUI 必须对其响应的一系列事件做出反应。有一些明显的问题:

  1. 事件本身具有不同的数据类型。随着程序的发展,事件将被添加、删除、更改。
  2. 如何桥接组成 GUI(不同的 UserControl)的多个类和抽象硬件的类。
  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);

    }
}

现在我们需要一些来自 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));
        }

这样做的优点是,如果事件引发和使用类型不匹配(此处为字符串文件名),编译器会发出错误消息。

可以进行许多改进,但这就是问题的核心。正如我在评论中所说,我很感兴趣,特别是如果有任何明显的遗漏/错误或缺陷。希望这对某人有帮助。

So, after some research based on the answers above, further Google searching and asking a colleague who knows a bit about C# my chosen solution to the problem is below. I remain interested in comments, suggestions and refinements.

First some further detail about the problem, which is actually pretty generic in the sense that the GUI is controlling something, that must remain wholly abstract, through a series of events to whose responses the GUI must react. There a a few distinct problems:

  1. The events themselves, with different data types. Events will get added, removed, changed as the program evolves.
  2. How to bridge several classes that comprise the GUI (different UserControls) and the classes that abstract the hardware.
  3. All classes can produce and consume events and must remain decoupled as far as possible.
  4. The compiler should spot coding cockups as far as possible (eg. an event that sends one data type but a comsumer that expects another)

The first part of this is the events. As the GUI and the device can raise several events, possibly having different data types associated with them, an event dispatcher is handy. This must be generic in both events and data, so:

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

    }
}

Now we need some events and coming from the C world, I like enums, so I define some events that the GUI will raise:

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

Now anywhere within the scope (namespace, typically) of the static class of the EventDispatcher it is possible to define a new dispatcher:

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

This creates an event handler for each event in the enum.

And consumed by subscribing to the event like this code in the constructor of the consuming Device object:

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

Where the InControlThread.Invoke is an abstract class that simply wraps the invoke call.

Events can be raised by the GUI simply:

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

This has the advantage that should the event raising and consuming types not match (here the string Filename) the compiler will grumble.

There are many enhancements that can be made but this is the nuts of the problem. I'd be interested, as I said in comments, especially if there are any glaring omissions/bugs or deficiencies. Hope this helps someone.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文