How do I update a GUI thread / class from a worker thread / class?
The first question is 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. Communication can take a long time and the device can return all kinds of errors.
The device is nicely abstracted into its own class when the GUI thread starts running in its own thread and has the usual open / close / read data / write functions. The GUI is also pretty simple - select a COM port, open, close, show read data or errors from device, allow modify and write, etc.
The question is how to update the GUI from the device class? There are several different types of data a device is associated with, so I need a relatively common bridge between the GUI class / thread and the worker device class / thread. In the GUI to the device everything works fine with [Begin] Invoke calls open / close / read / write, etc. In various events generated by the GUI.
I read a thread here (How do I update a GUI from another thread in C #?) Where the assumption is made that the GUI and worker thread are in the same class. Google searches force me to create a delegate or how to create a classic desktop, but that's not what I want at all, although they might be part of the solution. So, is there a simple yet general framework that can be used?
My C # level is moderate and I've been programming my whole life with the hint given. I'll figure it out (and post it back) ... Thanks in advance for any help.
a source to share
You can expose a public method in your UI that the device class can call on a background thread all the information it needs to pass to the UI. This public method will execute in the context of a background thread, but since it belongs to the UI class, you can now use any of the call marshaling methods you've read about.
Thus, the simplest option would be the following:
- add a method to your UI class (for example
MyUIForm
) called something likeUpdateUI()
that that uses whatever data structure you use to transfer data from the device to the UI you are using. You can declare this method in an interface (for exampleIUIForm
) if you want to later support DI / IoC and have an implementation of it. - on thread A (UI thread), your UI creates a device class, initializes all necessary settings, and starts a background thread. It also passes a pointer to itself.
- on stream B, the device collects data and calls
MyUIForm.UpdateUI()
(orIUIForm.UpdateUI()
). -
UpdateUI
mattersInvoke
orBeginInvoke
.
Note that you have the advantage of encapsulating all the UI and presentation logic in your UI. Your device class can now focus on working with hardware.
Update: To address scalability issues -
No matter how much your application grows and how many UI classes you have, you still want to cross the flow boundary using BeginInvoke for the specific UI class you want to update. (This UI class can be a specific control or the root of a specific visual tree, it really doesn't really matter). The main reason is that if you have multiple UI threads, you have to make sure that any UI updates happen on the thread, that UI was created based on the way Windows and windows messaging. Hence, the actual logic for crossing the boundary flow must be encapsulated in the UI layer.
Your device class shouldn't care about which UI classes and on which thread you need to update. In fact, I would personally make the device completely unaware of any user interface and simply output events to it that various user interface classes can subscribe to.
Note that an alternative solution is to make the threads completely encapsulated in the device class and make the UI clueless about the existence of the thread base. However, then crossing the thread boundary becomes the responsibility of the device class and must be contained in its logic, so you should not use the UI way to cross threads. It also means that your device class is bound to a specific UI thread.
a source to share
This is the event handler version.
This is simplified so there are no UI controls on the form and no properties on the SerialIoEventArgs class.
- Place your code to update the UI under the comment // Update UI
- Place your code to read serial I / O under the comment // Read from serial I / O
- Add fields / properties to the SerialIoEventArgs class and fill it in the OnReadCompleated method.
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
{
}
a source to share
So, after some research based on the answers above, further googling and asking a colleague who knows a little about C #, my chosen solution is below. I am still interested in comments, suggestions and clarifications.
First, some more details about the problem, which is actually quite typical in the sense that the GUI controls something that should remain completely abstract through a series of events that the GUI should respond to. There are several separate problems:
- The events themselves, with different data types. Events will be added, removed, changed as the program develops.
- How to connect multiple classes that make up the GUI (different UserControls) and classes that abstract the hardware.
- All classes can create and consume events and should be kept separate whenever possible.
- The compiler needs to identify the encoding devices as accurately as possible (for example, an event that sends one data type, but a comumer waiting for another)
The first part of this event. Since the GUI and device can generate multiple events, possibly associated with different data types, the event dispatcher is handy. This should be common for 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 in and out of the C world, I like enums, so I define some events that will boost the GUI:
public enum DEVICE_ACTION_REQUEST
{
LoadStuffFromXMLFile,
StoreStuffToDevice,
VerifyStuffOnDevice,
etc
}
Now, within the scope (usually the namespace) of the static EventDispatcher class, a new dispatcher can be defined:
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 enumeration.
And it is consumed by subscribing to the event like this code in the constructor of the consumer device object:
public DeviceController( )
{
EventDispatcher<DEVICE_ACTION_REQUEST>.Subscribe(DEVICE_ACTION_REQUEST.LoadAxisDefaults, (s, e) =>
{
InControlThread.Invoke(this, () =>
{
ReadConfigXML(s, (EventArgs<string>)e);
});
});
}
Where InControlThread.Invoke is an abstract class that just wraps the call invocation.
Events can only be raised by the 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));
}
This has the advantage that if the event creation and consumption types do not match (here the Filename line) the compiler will grumble.
There are many improvements to be made, but these are nuts of the problem. I would be interested, as I said in the comments, especially if there are any glaring omissions / errors or shortcomings. Hope this helps someone.
a source to share