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.

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:

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

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

Leave a Comment