Avatar of deleyd
deleydFlag for United States of America asked on

Get back to UI thread from ThreadPool thread?

I have a very simple sample project which displays the problem I'm having in a much larger project. Here's the entire code:
It has a MainWindow.xaml with a single button:
<Window x:Class="WpfApp7TestAsyncUI.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"
        xmlns:local="clr-namespace:WpfApp7TestAsyncUI"
        mc:Ignorable="d"
        Title="MainWindow" Height="250" Width="400">
    <Grid>
        <Button Content="Button" HorizontalAlignment="Left" Margin="160,72,0,0" VerticalAlignment="Top" Width="75" Click="Button_Click"/>

    </Grid>
</Window>

Open in new window

MainWindow.xaml.cs
using System.Windows;

namespace WpfApp7TestAsyncUI
{
    public partial class MainWindow : Window
    {
        private MainWindowViewModel viewModel;
        public MainWindow()
        {
            this.viewModel = new MainWindowViewModel();
            InitializeComponent();
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            this.viewModel.HandleButtonClick();
        }
    }
}

Open in new window

MainWindowViewModel.cs
using System.Threading.Tasks;

namespace WpfApp7TestAsyncUI
{
    public class MainWindowViewModel
    {
        public async void HandleButtonClick()
        {
            await Task.Delay(100).ConfigureAwait(false);
            Window1 window1 = new Window1();
        }
    }
}

Open in new window

There's also a simple Window1.xaml which is empty:
<Window x:Class="WpfApp7TestAsyncUI.Window1"
        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"
        xmlns:local="clr-namespace:WpfApp7TestAsyncUI"
        mc:Ignorable="d"
        Title="Window1" Height="450" Width="800">
    <Grid>
        
    </Grid>
</Window>

Open in new window

Window1.xaml.cs
using System.Windows;

namespace WpfApp7TestAsyncUI
{
    /// <summary>
    /// Interaction logic for Window1.xaml
    /// </summary>
    public partial class Window1 : Window
    {
        public Window1()
        {
            InitializeComponent();
        }
    }
}

Open in new window

That's it. The problem is in MainWindowViewModel.HandleButtonClick()
        public async void HandleButtonClick()
        {
            await Task.Delay(100).ConfigureAwait(false);
            Window1 window1 = new Window1();
        }

Open in new window

Here the await Task.Delay(100).ConfigureAwait(false); causes the next line to resume on a ThreadPool thread. From that ThreadPool thread, I need a way to get back to the UI thread so I can create and display a window. So far I haven't found a way.

There is no Dispatcher so I can't do Dispatcher.Invoke().

The sample code here currently bombs complaining that I'm not on a STA thread.

I'm trying to rescue a large project here I inherited (they always give me the Legacy code that doesn't work anymore) and this is basically the problem it's currently having. I'm wondering how I can rescue it by gradually refactoring it, rather than doing a major overhaul. Is there a way to get back to the UI thread from this spot where I'm trying to create Window1?

(Oh yes, I can't take the simple "Remove the .ConfigureAwait(false);" If I ran everything on the UI thread yes it would work but the UI wouldn't be responsive. The problem is how to display a window with a message that asks for User Input when that code is buried deep in code that's running on a background or ThreadPool thread. -- Without doing a major rewrite. [Would like to slowly migrate towards how it should be, rather than one risky major rewrite step.])
* WPFC#

Avatar of undefined
Last Comment
kaufmed

8/22/2022 - Mon
Snarf0001

Though I'm obviously not sure what other requirments you have - I think you simply need:

await Task.Delay(100).ConfigureAwait(true);

If you look at the description for the actual parameter:
continueOnCapturedContext: true to attempt to marshal the continuation back to the original context captured;

Which is exactly what you want.  After the continuation, you want the system to marshal you back to the calling context, which is the main ui thread from the button click handler.
kaufmed

First, "ConfigureAwait(true)" is effectively the same thing as removing "ConfigureAwait(false)". See:

https://devblogs.microsoft.com/dotnet/configureawait-faq/

Second, you generally don't use "ConfigureAwait(false)" at the UI level. "ConfigureAwait(false)" is for library code, where you don't care what context you are on. See the same article.
ASKER
deleyd

Don't focus on the .ConfigureAwait(); my real question is how to get back to the UI thread from a ThreadPool thread.
I started with Experts Exchange in 2004 and it's been a mainstay of my professional computing life since. It helped me launch a career as a programmer / Oracle data analyst
William Peck
kaufmed

If I ran everything on the UI thread yes it would work but the UI wouldn't be responsive.
I don't understand this comment. Why do you think that the app would be unresponsive by removing "ConfigureAwait(false)"? That method doesn't make or break something async...it just says to ignore there being a context.
kaufmed

my real question is how to get back to the UI thread from a ThreadPool thread.
Why do you think that the code is running on a ThreadPool thread? Are you doing an explicit Task.Run somewhere?
ASKER
deleyd

I found a solution. I need to save the SynchronizationContext, which the class has, in the class, and use it later on when I need to jump back to the UI thread.

using System;
using System.Threading;
using System.Threading.Tasks;

namespace WpfApp6Sync
{
    public class ViewModel
    {
        private SynchronizationContext synchronizationContext;
        public ViewModel()
        {
            this.synchronizationContext = SynchronizationContext.Current;
        }
        public async Task HandleButtonClick()
        {
            await Task.Delay(100).ConfigureAwait(false);
            synchronizationContext.Send(new SendOrPostCallback(this.MakeWindow), null);
        }

        private void MakeWindow(object state)
        {
            Window1 window = new Window1();
            window.Show();
        }
    }
}

Open in new window

Get an unlimited membership to EE for less than $4 a week.
Unlimited question asking, solutions, articles and more.
ASKER CERTIFIED SOLUTION
deleyd

Log in or sign up to see answer
Become an EE member today7-DAY FREE TRIAL
Members can start a 7-Day Free trial then enjoy unlimited access to the platform
Sign up - Free for 7 days
or
Learn why we charge membership fees
We get it - no one likes a content blocker. Take one extra minute and find out why we block content.
See how we're fighting big data
Not exactly the question you had in mind?
Sign up for an EE membership and get your own personalized solution. With an EE membership, you can ask unlimited troubleshooting, research, or opinion questions.
ask a question
kaufmed

I need to save the SynchronizationContext
Which is exactly what await does when you don't use "ConfigureAwait(false)".