What you want to do before doing multiple UI threads in WPF

6 minute read

First conclusion

If you want to do multiple UI threads in WPF, don’t do it …
If there is another workaround for the problem you are trying to avoid with multiple UI threads, consider that.
If you do it with a light feeling like “Oh? Isn’t it a solution if you make another UI thread?”, If you do not program carefully, you will die if you run for a long time, or you will die in some way. I have the impression that it often happens.

Text

I think WPF, or platforms that have most UIs, have a single UI thread, where there is a loop to handle events (messages).

As far as I know, UWP has a separate UI thread for each Window.

What makes me happy when I have multiple UI threads?

Heavy processing and modal dialogs do not affect anyone running in another UI thread.
Perhaps UWP does this because it’s less likely to freeze the UI if there is a UI thread for each window.

What happens with WPF?

Since it is a single UI thread, blocking all UI threads will freeze all windows in your app.
Let’s try it. Create a WPF app project and place a button on the MainWindow as shown below.

MainWindow.xaml


<Window
    x:Class="WpfApp5.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="MainWindow"
    Width="800"
    Height="450"
    mc:Ignorable="d">
    <StackPanel>
        <Button Click="ModalButton_Click" Content="Modal" />
        <Button Click="NonModalButton_Click" Content="NonModal" />
        <Button Click="FileDialogButton_Click" Content="FileDialog" />
    </StackPanel>
</Window>

Then, implement the click event in the code-behind as follows.

csharp:MainWindow.xaml.cs


using Microsoft.Win32;
using System.Windows;

namespace WpfApp5
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void ModalButton_Click(object sender, RoutedEventArgs e)
        {
            new MainWindow().ShowDialog();
        }

        private void NonModalButton_Click(object sender, RoutedEventArgs e)
        {
            new MainWindow().Show();
        }

        private void FileDialogButton_Click(object sender, RoutedEventArgs e)
        {
            new OpenFileDialog().ShowDialog();
        }
    }
}

Every time I press a button, the number of windows increases or a dialog to open a file appears. However, when the ModalButton_Click process runs, I can only operate the newly displayed Window. This isn’t a UI thread blocked, it’s a move that only touches the Window displayed in ShowDialog.

With an app that has a lot of windows, I think there are cases where it would be a problem if you just showDialog a window somewhere and other windows cannot be touched.

WPF Window can be used in another thread if it is a Single Thread Apartment, so ShowDialog is okay.

private void ModalButton_Click(object sender, RoutedEventArgs e)
{
    var t = new Thread(_ => new MainWindow().ShowDialog());
    t.SetApartmentState(ApartmentState.STA); //Mandatory
    t.Start();
}

If you do this, the new Window will run in a separate thread, so any heavy processing or blocking of threads beyond that will not hinder the movement of the original Window.

However, with this method, there is a problem that a memory leak occurs unless Dispatcher is terminated.
Let’s take a quick look. Debug in VS 2019 and take a snapshot of memory. After that, normally display three windows, close all of them, and then run the GC to take a snapshot of the memory.

If you take the Diff of the number of instances of the MainWindow class, it is 0. It seems that the closed Window has been recovered.

image.png

Next, try the same procedure by displaying it in a new UI thread.
increasing….

image.png

It will be like that. The same thing is written in this article.

http://grabacr.net/archives/1851

Now, let’s deal with it according to the coping method.

private void ModalButton_Click(object sender, RoutedEventArgs e)
{
    var t = new Thread(_ =>
    {
        var w = new MainWindow();
        w.Closed += (_, __) =>
        {
            //Exit Dispatcher when Window closes
            Dispatcher.CurrentDispatcher.BeginInvokeShutdown(DispatcherPriority.SystemIdle);
            Dispatcher.Run();
        };
        w.ShowDialog();
    });
    t.SetApartmentState(ApartmentState.STA); //Mandatory
    t.Start();
}

It disappeared safely.

image.png

The pain point

WPF’s DataGrid and ListBox can easily die if a collection change event comes from outside the UI thread. Therefore, it is troublesome if Windows belonging to another thread refers to the same collection and displays it on the screen.

In the first place, the standard ʻObservableCollection `itself is not thread-safe, so if you operate from multiple threads without thinking about anything, sometimes it will not work well and you will die.

So it seems to be painful if you do not duplicate the collection and synchronize each one in a good way or do something like that and publish the collection change event in the UI thread of the Window where you are displayed.

I hate it because I don’t want to create a situation where multiple threads have to work together if possible.

Another workaround

If you don’t want the window to be locked more than you think with the default ShowDialog, you can control the window so that it doesn’t move. For example, this will not touch the UI of the parent Window until the child Window is closed.

private void ModalButton_Click(object sender, RoutedEventArgs e)
{
    var originalWindowStyle = WindowStyle;
    WindowStyle = WindowStyle.None;
    IsEnabled = false;
    var w = new MainWindow { Owner = this };
    w.Closed += (_, __) =>
    {
        WindowStyle = originalWindowStyle;
        IsEnabled = true;
    };
    w.Show();
}

image.png

You can slam it into the Windows API to make it look more like real behavior than setting WindowStyle to None. For example, if you do the following, you will not be able to move, maximize, minimize, or close the child window until it is closed.

csharp:MainWindow.xaml.cs


using System;
using System.Windows;
using System.Windows.Interop;

namespace WpfApp1
{
    public partial class MainWindow : Window
    {
        private bool _isLock;
        public MainWindow()
        {
            InitializeComponent();
        }

        private void ShowButton_Click(object sender, RoutedEventArgs e)
        {
            _isLock = true;
            IsEnabled = false;
            var w = new MainWindow { Owner = this };
            w.Closed += (_, __) =>
            {
                _isLock = false;
                IsEnabled = true;
            };
            w.Show();
        }

        const int WM_SYSCOMMAND = 0x0112;
        const int SC_MOVE = 0xF010;
        const int SC_MASK = 0xFFF0;
        const int SC_MAXIMIZE = 0xF030;
        const int SC_MINIMIZE = 0xF020;
        const int WM_CLOSE = 0x0010;

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            var source = HwndSource.FromHwnd(new WindowInteropHelper(this).Handle);
            source.AddHook(WndProc);
        }

        private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
        {
            // _Do not close, maximize, minimize or move while isLock is true
            if (msg == WM_CLOSE)
            {
                handled = _isLock;
            }

            if (msg == WM_SYSCOMMAND)
            {
                var sc = wParam.ToInt32() & SC_MASK;
                if (sc is SC_MOVE or SC_MAXIMIZE or SC_MINIMIZE) // C# 9.Since it is written as 0, if it is earlier than that,==When||Write using
                {
                    handled = _isLock;
                }
            }
            return IntPtr.Zero;
        }
    }
}

image.png

I have to look up Windows messages, but I’m happy compared to the pain of multithreaded programming.

Heavy processing

There are other processes that may cause the UI to freeze, but there are cases where the UI thread performs heavy processing.
As much as possible, use async / await to do it asynchronously, and do not stop the UI thread by doing only heavy processing in another thread.

By doing this, you’ll probably be able to do a good deal with a single UI thread.

Summary

I personally feel that it would be totally happier to keep multiple UI threads in WPF as a last resort and find a way to do as little as possible.
Especially in addition to the difficulty of ordinary multithreaded programming

–Memory leak if Dispatcher is not shut down
–Die when an event done in another UI thread propagates

I’m addicted to such things, so I want to cry. There is a memory leak, so check all the places where you can open a window or create a thread. Personally, I want to cry when asked to make sure Dispatcher is shut down on all routes.

Also, ReactiveProperty is created on the assumption that there is only one UI thread, so when using ReactiveProperty, do not create multiple UI threads. Maybe it will be hard.

I’m automatically dispatching events to the UI thread, but since this dispatch destination is the UI thread that the app has from the beginning, I don’t know about other UI threads.

Tags: ,

Updated: