How to update UI from another thread in C#

By FoxLearn 12/21/2024 4:11:43 AM   113
In C#, you cannot directly update the UI from a background thread because Windows Forms and WPF (UI frameworks) require UI updates to happen on the main UI thread.

If you try to modify UI controls from another thread, you'll encounter exceptions, such as InvalidOperationException in Windows Forms.

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

In this article, we will explore how to execute multiple threads concurrently, update the UI based on their results, and avoid the "cross-thread operation" error by using BeginInvoke (or Invoke), which ensures that UI updates are performed on the UI thread.

Update the UI from Another Thread in C# Windows Forms Application

In Windows Forms (WinForms) applications, UI controls, such as TextBox, Label, or DataGridView, are created on the UI thread and can only be updated from that same thread. If you try to update the UI from a different thread, you'll encounter the exception shown above.

This can be done using the Control.BeginInvoke or Control.Invoke methods.

  • Invoke: This method is synchronous, meaning the calling thread will wait for the UI thread to finish updating before continuing.
  • BeginInvoke: This method is asynchronous, meaning the calling thread can continue executing without waiting for the UI update.

We simulate running several tasks concurrently. Each task represents a simulated operation (e.g., a delayed HTTP GET request or a computational task). The results of these tasks will be logged in a TextBox as each task completes.

The form contains a TextBox (txtLog) to display log messages, a Button (btnStartThreads) to start the tasks, and a NumericUpDown (numThreads) to allow the user to specify how many threads to run.

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

namespace ThreadingExample
{
    public partial class frmUpdateUIThread : Form
    {
        Control control;

        public frmUpdateUIThread()
        {
            InitializeComponent();
            control = txtLog; // This can be any control, like a TextBox
        }

        // Method to log messages to the UI thread
        private void Log(string msg)
        {
            string m = $"{DateTime.Now.ToString("H:mm:ss.fffff")}\t{msg}\n";
            control.BeginInvoke((MethodInvoker)delegate ()
            {
                txtLog.AppendText(m); // Append log message to the TextBox
                txtLog.ScrollToCaret(); // Scroll to the latest log entry
            });
        }

        // Start multiple threads and log their results in the UI
        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 delay = TimeSpan.FromMilliseconds(random.Next(1000, 5000)); // Simulate random work time

                var task = Task.Run(async () =>
                {
                    var tId = $"Task ID {j}, ThreadID = {Thread.CurrentThread.ManagedThreadId}";

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

                    await Task.Delay(delay); // Simulate some background work

                    Log($"{tId} finished. Took {delay.TotalSeconds} seconds");
                });

                allTasks.Add(task);
            }

            // Wait for all tasks to finish
            await Task.WhenAll(allTasks); // This ensures that the main thread waits for all background tasks to complete before logging the final message.

            // Final log entry after all tasks are complete
            Log("All tasks have finished");
        }
    }
}

In the constructor, we assign the TextBox control (txtLog) to the control variable. The Log method takes a message, appends a timestamp, and uses BeginInvoke to safely update the TextBox on the UI thread.

In the btnStartThreads_Click method, we create several background tasks (Task.Run). Each task runs a simple loop with a random delay to simulate background work (e.g., a network request or computational task). The task logs a message to the UI thread when it starts and when it finishes.

BeginInvoke ensures that each log message is added to the TextBox on the UI thread. This prevents the cross-thread operation error and ensures that the UI remains responsive.

Always remember that UI controls can only be updated on the UI thread. If you need to update the UI based on results from background threads, always use Invoke or BeginInvoke.

While BeginInvoke is asynchronous, the method is still relatively efficient. However, you should avoid excessive UI updates (e.g., updating every millisecond) because frequent UI updates can cause the application to become unresponsive.

In applications where you need to run multiple tasks concurrently and update the UI based on their results, you must marshal the UI updates back to the UI thread. In WinForms, this can be done easily using Control.BeginInvoke or Control.Invoke.