2

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

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

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

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

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

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

4

3 回答 3

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 类可以订阅的事件。

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

于 2010-04-13T07:20:19.510 回答
0

这是一个带有事件处理程序的版本。
它被简化,因此表单中没有 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
{
}
于 2010-04-13T08:40:04.637 回答
0

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

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

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

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

并通过在消费设备对象的构造函数中订阅类似以下代码的事件来消费:

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

这样做的好处是,如果事件引发和消费类型不匹配(这里是字符串文件名),编译器会抱怨。

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

于 2010-04-19T09:03:50.270 回答