We help IT Professionals succeed at work.

Get back to UI thread from ThreadPool thread?

deleyd
deleyd asked
on
18 Views
Last Modified: 2020-04-15
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.])
Comment
Watch Question

CERTIFIED EXPERT

Commented:
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.
CERTIFIED EXPERT
Most Valuable Expert 2011
Top Expert 2015

Commented:
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.
deleydSoftware Engineer

Author

Commented:
Don't focus on the .ConfigureAwait(); my real question is how to get back to the UI thread from a ThreadPool thread.
CERTIFIED EXPERT
Most Valuable Expert 2011
Top Expert 2015

Commented:
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.
CERTIFIED EXPERT
Most Valuable Expert 2011
Top Expert 2015

Commented:
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?
deleydSoftware Engineer

Author

Commented:
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

Software Engineer
Commented:
This one is on us!
(Get your first solution completely free - no credit card required)
UNLOCK SOLUTION
CERTIFIED EXPERT
Most Valuable Expert 2011
Top Expert 2015

Commented:
I need to save the SynchronizationContext
Which is exactly what await does when you don't use "ConfigureAwait(false)".
Unlock the solution to this question.
Join our community and discover your potential

Experts Exchange is the only place where you can interact directly with leading experts in the technology field. Become a member today and access the collective knowledge of thousands of technology experts.

*This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.

OR

Please enter a first name

Please enter a last name

8+ characters (letters, numbers, and a symbol)

By clicking, you agree to the Terms of Use and Privacy Policy.