这里的第一个问题让大家好。
我正在处理的要求是一个小型测试应用程序,它通过串行端口与外部设备进行通信。通信可能需要很长时间,设备可以返回各种错误。
该设备在其自己的类中很好地抽象,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#水平是温和的,我一直在编程我的所有工作生活,给出一个线索我会弄清楚(并回复)...提前感谢任何帮助。
答案 0 :(得分:6)
您可以在UI类上公开设备类可以在后台线程上调用的公共方法,其中包含传递给UI所需的所有信息。该公共方法将在后台线程的上下文中执行,但由于它属于UI类,因此您现在可以使用您已阅读的任何调用编组技术。
因此,最简单的设计是:
MyUIForm
),类似于UpdateUI()
,它采用您使用的任何数据结构将数据从设备传递到您使用的UI。您可以在接口中声明该方法(例如IUIForm
),如果您想稍后支持DI / IoC,并让表单实现它。MyUIForm.UpdateUI()
(或IUIForm.UpdateUI()
)。UpdateUI
视情况Invoke
或BeginInvoke
。请注意,在UI类中封装所有UI和表示逻辑具有附带的好处。您的设备类现在可以专注于处理硬件。
更新:解决您的可扩展性问题 -
无论您的应用程序增长多少以及您拥有多少UI类,您仍然希望使用BeginInvoke跨越线程边界来处理您要更新的特定UI类。 (那个UI类可能是一个特定的控件或特定可视化树的根,它并不重要)主要原因是如果你有多个UI线程,你必须确保在线程上发生任何UI的更新由于Windows消息传递和Windows的工作方式,因此创建了此特定用户界面。因此,跨越边界线程的实际逻辑应该封装在UI层中。
您的设备类不应该关心哪些UI类以及需要更新的线程。事实上,我个人会让设备完全无视任何UI,并且只会在其上公开不同UI类可以订阅的事件。
请注意,替代解决方案是使线程完全封装在设备类中,并使UI不知道是否存在bacground线程。然而,然后线程边界交叉成为设备类的责任并且应该包含在其逻辑中,因此您不应该使用跨越线程的UI方式。这也意味着您的设备类绑定到特定的UI线程。
答案 1 :(得分:0)
这是一个带有事件处理程序的版本
它被简化,因此表单中没有UI控件,而且SerialIoEventArgs类中没有属性。
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必须作出反应。有一些不同的问题:
第一部分是事件。由于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),编译器会发牢骚。
可以进行许多增强,但这是问题的关键。正如我在评论中所说,我会感兴趣,特别是如果有任何明显的遗漏/错误或缺陷。希望这有助于某人。