Accessing the user interface from a background thread in WPF

by Trent Guidry 11. June 2009 15:01

 One issue that comes up when creating multithreaded applications in WPF is how does one go about updating the user interface from a non UI thread.

The short answer is easy, you can’t update the UI directly from a non UI thread and if you try WPF throws an exception. To see this, consider the code below.

XAML

<Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <TextBlock Text="0" Name="tbResults" Grid.Row="0" />
        <Button Content="Start" Name="btnStart" Grid.Row="1" Click="btnStart_Click" />
    </Grid>
 

C#

using System.Windows;
using System.ComponentModel;
using System.Collections.ObjectModel;
using System.Threading;
using System;

namespace WinApp1
{
    public partial class Window1 : Window
    {
        ThreadedTask _threadedTask = new ThreadedTask();

        public Window1()
        {
            InitializeComponent();
        }

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            _threadedTask.UpdateUserInterface += new EventHandler<UpdateEventArgs>(threadedTask_UpdateUI);
        }

        void threadedTask_UpdateUI(object sender, UpdateEventArgs e)
        {
            tbResults.Text = e.Counter.ToString();
        }

        private void btnStart_Click(object sender, RoutedEventArgs e)
        {
            _threadedTask.StartTask();
        }
    }

    public class ThreadedTask
    {
        public event EventHandler<UpdateEventArgs> UpdateUserInterface;

        public ThreadedTask()
        {
        }

        public void StartTask()
        {
            ThreadStart threadStart = new ThreadStart(Task);
            Thread thread = new Thread(threadStart);
            thread.Start();
        }

        private void Task()
        {
            for (int i = 0; i < 100; i++)
            {
                Thread.Sleep(100);
                FireUpdateUserInterface(i);
            }
        }

        private void FireUpdateUserInterface(int nCounter)
        {
            if (UpdateUserInterface != null)
            {
                UpdateUserInterface(this, new UpdateEventArgs(nCounter));
            }
        }
    }

    public class UpdateEventArgs : EventArgs
    {
        private int _counter;

        public UpdateEventArgs()
            : base()
        {
        }

        public UpdateEventArgs(int counter)
            : this()
        {
            _counter = counter;
        }

        public int Counter
        {
            get { return _counter; }
            set { _counter = value; }
        }
    }
}

Basically, what is going on here is that there is a button and a label on the main window.  The main window also contains an instance of a class ThreadedTask that will run the thread, which is called Task.  This class has a method called StartTask, which creates the thread and starts it.  This class also exposes an event called UpdateUserInterface that will be fired to update the user interface and passes an EventArgs derived class called UpdateEventArgs that stores the data to display in the user interface.  The window subscribes to this event on load and when it is fired, it updates the textblock tbResults with the string value of the integer stored in the event arguments.  The threaded function in this case is a simple for loop that counts from 1 to 100, sleeps 100 milliseconds per iteration, and then calls FireUpdateUserInterface with the thread count to update the user interface. 

This seems simple enough, however, when it is run and the button is pressed, the application throws the exception below:

An unhandled exception of type 'System.InvalidOperationException' occurred in WindowsBase.dll

Additional information: The calling thread cannot access this object because a different thread owns it.

This is actually a pretty straightforward error message, basically it is saying that you can’t access the UI directly using a background thread.

The easiest way to get around this and to allow the background thread to update the UI using the update event is to modify the way that the event is fired using the Dispatcher to get it fired on the UI thread. 

This can be done by changing:

     FireUpdateUserInterface(i);

To:

           Application.Current.Dispatcher.BeginInvoke(new ThreadStart(() => FireUpdateUserInterface(i)), null);

Here I am telling the Dispatcher to invoke FireUpdateUI on the UI thread.  I am also using a Lambda expression to turn the FireUpdateUserInterface with an integer argument into a function with no arguments. 

Running this results in the originally intended effect of updating the UI from the worker thread. 

Here is the full C# code: 

using System.Windows;
using System.ComponentModel;
using System.Collections.ObjectModel;
using System.Threading;
using System;

namespace WinApp1
{
    public partial class Window1 : Window
    {
        ThreadedTask _threadedTask = new ThreadedTask();

        public Window1()
        {
            InitializeComponent();
        }

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            _threadedTask.UpdateUserInterface += new EventHandler<UpdateEventArgs>(threadedTask_UpdateUI);
        }

        void threadedTask_UpdateUI(object sender, UpdateEventArgs e)
        {
            tbResults.Text = e.Counter.ToString();
        }

        private void btnStart_Click(object sender, RoutedEventArgs e)
        {
            _threadedTask.StartTask();
        }

    }
    public class ThreadedTask
    {
        public event EventHandler<UpdateEventArgs> UpdateUserInterface;

        public ThreadedTask()
        {
        }

        public void StartTask()
        {
            ThreadStart threadStart = new ThreadStart(Task);
            Thread thread = new Thread(threadStart);
            thread.Start();
        }

        private void Task()
        {
            for (int i = 0; i < 100; i++)
            {
                Thread.Sleep(100);
                Application.Current.Dispatcher.BeginInvoke(new ThreadStart(() => FireUpdateUseInterface(i)), null);
            }
        }


        private void FireUpdateUseInterface(int counter)
        {
            if (UpdateUserInterface != null)
            {
                UpdateUserInterface(this, new UpdateEventArgs(counter));
            }
        }
    }

    public class UpdateEventArgs : EventArgs
    {
        public UpdateEventArgs()
            : base()
        {
        }

        public UpdateEventArgs(int nCounter)
            : this()
        {
            _counter = nCounter;
        }

        private int _counter;
        public int Counter
        {
            get { return _counter; }
            set { _counter = value; }
        }
    }
}

Tags: , ,

Add comment




  Country flag

biuquote
  • Comment
  • Preview
Loading