This is a WinForms application with a class that have several methods taking lot of time, those methods raise events at certain interval that I use to update the GUI, change labels, texts, colors, and so forth.
The Tasks run on a different thread, so they are not blocking the UI, that is fine, since I either call them from a System.Timers.Timer
or create a list of Tasks and run them async.
The problem I am having is that, on the event handler I created for that class that reports me different status I cannot update the GUI, because of a cross-thread operation, so what I did to solve this is check if InvokeRequired
on the EventHandler
and then BeginInvoke
.
That did the trick, but I'm sure this is not right, my EventHandler can be called hundred of times and each time I begin a new Invoke
. What is the best approach to do?
I am not experienced with Delegates and somehow I believe I should be using them, not sure how
I leave here a sample piece of code that pretty much simplifies the workload and issue
public partial class Form1 : Form {
private Operational o = new();
private System.Timers.Timer WorkerTimer=new() { Interval=1000,Enabled=false};
public Form1() {
InitializeComponent();
o.OperationComplete += OperationCompleteEventHandler;
WorkerTimer.Elapsed += MonitorExecutor;
}
private async void MonitorExecutor(object? sender, ElapsedEventArgs e) {
List<Task> myTasks = new();
myTasks.Add(Task.Run(o.DoWork));
myTasks.Add(Task.Run(o.DoWork));
await Task.WhenAll(myTasks);
// Report all tasks have completed
}
private void OperationCompleteEventHandler(object? sender, EventArgs e) {
if (InvokeRequired) {
// Otherwise it will throw a cross-thread exception
Debug.WriteLine("Invoked Required!");
BeginInvoke(() => OperationCompleteEventHandler(sender, e));
return;
}
label1.Text = "WorkCompleted";
// But this could also take a lot of time,
// so I don't want this method to hang my thread
Thread.Sleep(500);
}
private void button1_Click(object sender, EventArgs e) {
WorkerTimer.Enabled = !WorkerTimer.Enabled;
button1.Text = WorkerTimer.Enabled ? "Running" : "Stopped";
}
}
public class Operational {
public event EventHandler? OperationComplete;
public void DoWork() {
// Long Operations
Thread.Sleep(500);
OperationComplete?.Invoke(this, new());
}
}
You are not using await
quite right. Given that WinForms sets up a SynchronizationContext
, you should rely on that to marshal continuations (the bit after the await
) onto the correct thread.
There are a number of different ways to do this, but primarily you need to move the invocation of OperationComplete
to a normal await
, and run the rest of the code using Task.Run
.
public async Task DoWork()
{
await Task.Run(DoWorkCore);
OperationComplete?.Invoke(this, new());
}
private void DoWorkCore()
{
// Long Operations
Thread.Sleep(500);
}
Then you can remove the whole if (InvokeRequired) {
block, because now OperationCompleteEventHandler
will always run on the UI thread.
private async void OperationCompleteEventHandler(object? sender, EventArgs e)
{
label1.Text = "WorkCompleted";
await Task.Run(() => {
// some other long running stuff
Thread.Sleep(500);
});
// more UI stuff here
}
And MonitorExecutor
can simply be
private async void MonitorExecutor(object? sender, ElapsedEventArgs e)
{
List<Task> myTasks = new();
myTasks.Add(o.DoWork);
myTasks.Add(o.DoWork);
await Task.WhenAll(myTasks);
// Report all tasks have completed
}
If you don't want to change DoWork
then you will need to use some kind of Event-to-Task conversion such as A reusable pattern to convert event into task or General purpose FromEvent method