Delayed Synchronous UI Updates

During my last internship, I was working on a WPF (Windows Presentation Foundation) application for 3D modeling. One particular problem that I encountered was handing UI updates.

Like most UI frameworks, event listeners in the WPF framework run on the main UI thread. In our application, we had a form GUI that controls the parameters for a 3D model and any changes in the form need to automatically update the 3D model preview. Unlike most WPF applications that seem to be just glorifed excel documents (at least to me personally), our event listeners that update the 3D model preview took a significant amount of time to run (approximately 300-800 ms).

Now you may be wondering why didn’t we just offload the 3D model calculations to a background thread and update the UI when it finishes? At first I asked the same question. It turns out that due to how WPF heavily couples the underlying numbers (ViewModel) to the GUI (View), I would need to refactor a signficant amount of code in order to safetly pass the ViewModel data to a separate thread. In other words, this obscure detail basically rendered our existing architecture moot. Unfortunately for me, I did not have the luxury of time to refactor our 3D model calculation module.

Personally, I think freezing the UI for less than 1 second might be negligible. However, when the user triggers a stream of events such as using their mouse wheel on an IntegerUpDownSpinner (you can quickly increase or decrease the value with your mousewheel), then the UI will lag for a long time as it handles all the backlogged events.

IntegerUpDown control from the Extended WPF Toolkit

One thing I realized is that every event except the last one is relevant because the final 3D model is only dependant on the final values in the form. With this in mind, I thought about spawnning a separate thread that waits until there’s probably no more input before running the 3D model calculations.

First let’s look at the traditional code that lags the UI:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        mValueInput.ValueChanged += mValueInput_ValueChanged;
    }

    void mValueInput_ValueChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
    {
        update();
    }

    void update()
    {
        // Suppose this method takes a couple hundred milliseconds to run
        string message = "The new value is: " + mValueInput.Value;
        Console.WriteLine(message);
        mResultLabel.Content = message;
    }
}

My first attempt to solve this problem is by spawning a separate thread that waits 500 ms before starting the 3D model calculations:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        mValueInput.ValueChanged += mValueInput_ValueChanged;
    }

    bool mIsWaitingForInputToStop = false;

    void mValueInput_ValueChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
    {
        if (mIsWaitingForInputToStop)
        {
            // Do nothing
        }
        else
        {
            mIsWaitingForInputToStop = true;
            
            (new Thread(() =>
            {
                Thread.Sleep(500);
                
                Application.Current.Dispatcher.Invoke(new Action(() =>
                {
                    // Run in main thread
                    update();
                    mIsWaitingForInputToStop = false;
                }));
            })).Start();
        }
    }

    void update()
    {
        // Suppose this method takes a couple hundred milliseconds to run
        string message = "The new value is: " + mValueInput.Value;
        Console.WriteLine(message);
        mResultLabel.Content = message;
    }
}

One thing to note in the above code is that mIsWaitingForInputToStop is only being read/written on the main UI thread so there’s no race conditions to worry about. However the problem with the above code is that the second thread doesn’t know at the end of its sleep if there were any new input events! All this does is trigger update() every 500 ms as long as there are active inputs. I suppose this is also desirable in some situations to give the user a live preview but it’s not exactly what I wanted.

To fix this, I need to use a second flag that lets me know if an event was recently triggered. If there was an event triggered after my second thread wakes up, then it needs to go back to sleep and reset that flag. Of course with 2 threads reading/writing to the same variable, I also need to add some mutexes to avoid race conditions.

Here is the source code to a working example of the snippet shown below. While I have tested the code below and have reasoned through it several times, I cannot guarantee that it will work 100% of the time. Like all concurrent programs, there may be some weird edge case that I have not encountered yet.

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        mValueInput.ValueChanged += mValueInput_ValueChanged;
    }

    bool mIsWaitingForInputToStop = false;
    bool mIsUserInputing = false;
    Mutex mWaitingLock = new Mutex();
    Mutex mInputingLock = new Mutex();

    void mValueInput_ValueChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
    {
        mInputingLock.WaitOne();
        mIsUserInputing = true;
        mInputingLock.ReleaseMutex();

        mWaitingLock.WaitOne();
        if (mIsWaitingForInputToStop)
        {
            // Do nothing
            mWaitingLock.ReleaseMutex();
        }
        else
        {
            mIsWaitingForInputToStop = true;
            mWaitingLock.ReleaseMutex();
            
            (new Thread(() =>
            {
                while (true)
                {
                    mInputingLock.WaitOne();
                    if (mIsUserInputing)
                    {
                        // Wait for user input to stop
                        mIsUserInputing = false;
                        mInputingLock.ReleaseMutex();
                    }
                    else
                    {
                        mInputingLock.ReleaseMutex();
                        break;
                    }

                    Thread.Sleep(500);
                }
                
                Application.Current.Dispatcher.Invoke(new Action(() =>
                {
                    // Run in main thread
                    update();

                    mWaitingLock.WaitOne();
                    mIsWaitingForInputToStop = false;
                    mWaitingLock.ReleaseMutex();
                }));
            })).Start();
        }
    }

    void update()
    {
        // Suppose this method takes a couple hundred milliseconds to run
        string message = "The new value is: " + mValueInput.Value;
        Console.WriteLine(message);
        mResultLabel.Content = message;
    }
}

Disclaimer: The above code is an implementation that I wrote from scratch today based on what I remembered about the problem and not what I wrote at work a few months ago.