Search code examples
c#multithreadingwinformstimerinvoke

Cross-Thread Operations and the use of Invoke repetitive times, best approach


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

}

Solution

  • 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