Updating controls, while performing blocking operations, can be tricky in Winforms. In the example below, we’ll show how to update TX-RX virtual LEDs pictureboxes, and at the same time, perform serial communication.
Introduction
As obsolete as it may seem, serial communication over RS232, RS485 or virtual USB COM ports still plays a role in the industry. Whether it is exchanging data with a CNC machine or other industrial machinery, or interfacing via Modbus with a HVAC equipment or appliance, serial communication is hidden but ubiquitous.
When designing the user interface of SCADAs or graphic panels, it is paramount to make sure proper visual updates to the user are constantly returned, even during long and excruciating data exchange session, or while performing time-consuming operations. Labels, picture boxes, gauges and other controls need to be constantly updated in the background.
One of the frequently asked features from my colleagues is to have a visual representation of RX/TX data over the serial line, as most of the cheap serial adapters and interfaces do not feature any data LEDs.
This article will show how to user the background workers for the UI update of two picture boxes mimicking a red and a green LEDs. The concept can be extended, of course, to all visual controls that need to be updated asynchronously.
Setting It Up
Step by Step Walk-through
We’ll start setting up the form. For this example, we just need a form with two pictureboxes, one for the TX LED and the other for the RX LED. We’ll also set their visibility to False
and resize them to properly resemble two LEDs in the up left corner of our form. We’ll call them pbSend
(TX) and pbReceive
(RX).
The next step is declaring the delegates and background workers needed. The UI update must be in the main thread, and because the background workers run on threads other than the main, we shall set up a delegate that will do that for us.
We’ll also declare the background workers themselves, one for the TX led, and the other for the RX led.
Private WithEvents sp As New SerialPort
Private Delegate Sub LEDSwitchDelegate(ByRef a As PictureBox, ByVal b As Boolean)
Private WithEvents backgroundWorker1 As New BackgroundWorker()
Private WithEvents backgroundWorker2 As New BackgroundWorker()
The actual sub that will provide switching ON and OFF the two LEDs is the one below. We’ll call it by passing the picturebox
name and its visibility as argument. For example:
LEDSwitch(pbSend, True)
It will cause the pbSend
LED to be switched ON. This will work on main thread and any other thread, thanks to the delegate!
Private Sub LEDSwitch(ByRef a As PictureBox, ByVal b As Boolean)
If Me.InvokeRequired Then
Dim d As New LEDSwitchDelegate(AddressOf Me.LEDSwitch)
Me.BeginInvoke(d, New Object() {a, b})
Else
Try
a.Visible = b
Catch ex As Exception
End Try
End If
End Sub
In my project, the LEDs shall switch ON for 500ms any time one or more bytes is sent or received, therefore the background worker shall run for that exact amount of time.
Private Sub backgroundWorker1_DoWork(ByVal sender As Object, _
ByVal e As DoWorkEventArgs) Handles backgroundWorker1.DoWork
Thread.Sleep(500)
End Sub
Private Sub backgroundWorker2_DoWork(ByVal sender As Object, _
ByVal e As DoWorkEventArgs) Handles backgroundWorker2.DoWork
Thread.Sleep(500)
End Sub
There’s not much they will do, except waiting for 500ms (here your mileage may vary and might make a better use of the background workers wasted cycles).
After the work is completed (just a 500ms non-blocking snooze), the LED shall be turned OFF again. We’ll make use of the RunWorkerCompleted
event associated with each of the two background workers.
Private Sub backgroundWorker1_RunWorkerCompleted(ByVal sender As Object, _
ByVal e As RunWorkerCompletedEventArgs) Handles backgroundWorker1.RunWorkerCompleted
LEDSwitch(pbSend, False)
End Sub
Private Sub backgroundWorker2_RunWorkerCompleted(ByVal sender As Object, _
ByVal e As RunWorkerCompletedEventArgs) Handles backgroundWorker2.RunWorkerCompleted
LEDSwitch(pbReceive, False)
End Sub
Ok, we switched OFF the LEDs, but how do we switch then ON in the first place, for example when data is received?
We’ll use a Sub
dedicated to that, and we will bind the Sub to the DataReceived
event of the Serialport
class instance. Because TransmittingData()
is on the main thread, we can switch ON the LED directly. We make sure the background workers aren’t already busy to avoid calling them multiple times.
Private Sub TransmittingData()
LEDSwitch(pbSend, True)
If Not backgroundWorker1.IsBusy Then
backgroundWorker1.RunWorkerAsync()
End If
End Sub
Private Sub ReceivingData()
LEDSwitch(pbReceive, True)
If Not backgroundWorker2.IsBusy Then
backgroundWorker2.RunWorkerAsync()
End If
End Sub
The last Sub
we need is just the form_Load
method, where we add the event handler of the DataReceived
event and bind it to ReceivingData()
.
Private Sub AsynchPictureboxes_Load(sender As Object, e As EventArgs) Handles MyBase.Load
AddHandler sp.DataReceived, AddressOf ReceivingData
End Sub
Unfortunately, the System.IO.Ports.SerialPort
class does not include a DataSent
event, so we can’t directly bind it, but this is easily overcome by the fact that whenever one or more bytes need to be transmitted, we would probably already have a Sub
for that. We’ll use the Sub
where the Serialport.Write
method is called to call the TransmittingData() Sub
too.
As seen, we do not need to worry to switch OFF the LEDs. The background workers will take care of that.
Further Developments
It would be possible to extend the methods:
- A port or data error can be displayed and identified, e.g., the two LEDs blinking rapidly at the same time. In this case, we can bind the event as follows, and write a specific
DataError
method to deal with such occurrence.
AddHandler sp.ErrorReceived, AddressOf DataError
- Further methods can be developed to display, e.g., CRC, Checksum errors, etc.
- The frequency of the LEDs blink can be changed programmatically, by passing the appropriate pulse time to the background workers, in response to specific messages or datagrams.
- One or more comboboxes and textboxes can be programmatically populated by background workers, e.g., in real time with data incoming from the serial communication, without keeping the main thread busy and unresponsive.
Conclusion and Points of Interest
The article was intended to show a quick and dirty way to update the UI from background workers, especially in conjunction with slow data rate transfers, or time-consuming operations that might keep the UI busy. The example provided can easily be extended and modified to suit the needs of programmers still dealing with Winforms technology, and in need to programmatically and frequently update multiple controls in a form, without blocking the user interaction with the application interface.
History
- 14th October, 2020: Initial version