How to update UI from another thread

I often need to be able to run multiple threads and update the UI based on the results. For example, I may need to execute GET requests to 10 different endpoints concurrently, and then report their results in a datagrid as they come back.

The problem is you can’t just update the UI from any thread. It must be on the UI thread. Otherwise you get the following error:

System.InvalidOperationException: ‘Cross-thread operation not valid: Control accessed from a thread other than the thread it was created on.’

The solution is to call control.BeginInvoke, passing in a MethodInvoker delegate. The code in the delegate will be executed on the UI thread, hence allowing you to update the UI. Just don’t call BeginInvoke/Invoke in the form constructor.

Note: BeginInvoke() is asynchronous. Invoke() is synchronous. You can use either.

Example app

I created a simple app that runs multiple threads at once, and logs the results in the UI as they finish. Here’s what it looks like:

WinForm - Several threads writing to a textbox (timestamp, thread id, and message)

Note: The threads aren’t doing anything useful, they’re just delaying for a random amount of time. This is to simulate real work being done, and simplifies the example.

Code

Here is the code showing how to update the UI from different threads with BeginInvoke:

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace ThreadingExample
{
    public partial class frmUpdateUIFromThread : Form
    {
        Control control;
        public frmUpdateUIFromThread()
        {
            InitializeComponent();
            control = txtLog; //this can be any control
        }
        private void Log(string msg)
        {
            string m = $"{DateTime.Now.ToString("H:mm:ss.fffff")}\t{msg}\n";
            control.BeginInvoke((MethodInvoker)delegate ()
            {
                txtLog.AppendText(m);
                txtLog.ScrollToCaret();
            });
        }
        private async void btnStartThreads_Click(object sender, EventArgs e)
        {
            Random random = new Random();
            List<Task> allTasks = new List<Task>();

            for (int i = 1; i <= (int)numThreads.Value; i++)
            {
                var j = i;
                var delayFor = TimeSpan.FromMilliseconds(random.Next(100, 5000));
                var task = Task.Run(async () => 
                {
                    var idForLog = $"Task ID {j}, ThreadID={Thread.CurrentThread.ManagedThreadId}";

                    Log($"{idForLog} starting processing");

                    await Task.Delay(delayFor);

                    Log($"{idForLog} finished. Took {delayFor.TotalSeconds}");
                });

                allTasks.Add(task);
            }

            await Task.WhenAll(allTasks);

            Log("All tasks have finished");
        }
    }
}

Code language: C# (cs)

Comments are closed.