0

I have a WinForms application, and I want to use the Windows.Forms.OpenFileDialog, which requires an STA thread. It works fine with threads, but what about tasks?

Related question, but didn't find a solution there: How to create a task (TPL) running a STA thread?

Here's an example of what I'm trying to achieve (mainly tried what is done in the related question above):

public async Task<string> TestFileDialogAsync()
{
    string outFile = "no file";

    await Task.Factory.StartNew(() =>
    {
        Console.WriteLine($"Thread apartment: {Thread.CurrentThread.ApartmentState}");
            // prints MTA
        try
        {
            using (OpenFileDialog myDialog = new OpenFileDialog()) 
            {
                myDialog.Title = "Choose a file...";
                myDialog.Filter = "All Files|*.*";

                if (myDialog.ShowDialog() == DialogResult.OK) // no dialog ever shown
                {
                    outFile = myDialog.FileName;
                }
            }
        }
        catch (Exception e) // no exception thrown - assuming task blocks in ShowDialog
        {
            Console.WriteLine($"Exception occured: {e.Message}");
            throw;
        }
    }, CancellationToken.None, TaskCreationOptions.None,
        TaskScheduler.FromCurrentSynchronizationContext());

    return outFile;
}

This is then called as:

string chosenFile = await TestFileDialogAsync();

In the question I've linked something similar is marked as answer, but this does not work for me.

I don't understand how TaskScheduler.FromCurrentSynchronizationContext() is supposed to force a task to run on a STA thread, as specified in the related question I've linked. (maybe the calling thread there was an STA thread to begin with, but the task was starting in MTA, and that is why this works?)

So how do I forcibly start a task on STA apartment state, so that I can use components that require it in my application?

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
Gregor Sattel
  • 352
  • 3
  • 12
  • Have you tried to move your dialog opening to continuation (as done in the linked answer)? Also usually using tasks/threads is to offload some work from the STA thread, so to me it does not make much sense to offload opening dialog to another thread in your code. – Guru Stron Jul 26 '21 at 14:37
  • 7
    UI belongs in the main thread - end of story. Servicing the UI is the main thread's **one and only job**. While it's not entirely impossible to use UI components in a separate thread with a separate message loop, it's a completely bonkers design that only has extremely limited legitimate use cases when you're forced to use badly designed components that you do not have source code for, which monopolize the UI thread, and that you absolutely cannot live without. This is a situation of technical debt you want to get out of rather than digging yourself deeper, so just don't do this. – J... Jul 26 '21 at 14:43
  • 5
    Note that it's *very* rarely a good idea to have multiple UI threads. Unless this is the entry point for your whole application, you should almost certainly solve your problem with only a single UI thread, rather than trying to create two. – Servy Jul 26 '21 at 14:44
  • I would suggest using the terms "UI thread" and "background thread" rather than STA/MTA. The later are terms in Component Object Model (COM), and are less commonly used in c#. – JonasH Jul 26 '21 at 15:00
  • Related: [Set ApartmentState on a Task](https://stackoverflow.com/questions/16720496/set-apartmentstate-on-a-task) – Theodor Zoulias Jul 26 '21 at 16:01
  • It seems your main difficulty is just failing to understand the guidance seen in the duplicates. I.e. `FromCurrentSynchronizationContext` needs to be done in the thread where you want your task eventually to run, not where you're trying to run it from. There is also the possibility you really do want a whole new STA thread, which is also addressed in the duplicates. – Peter Duniho Jul 26 '21 at 16:51
  • @PeterDuniho: Neither of the duplicates create an STA thread. They do create threads that *say* they are STA, but since they don't pump, they won't work correctly. – Stephen Cleary Jul 26 '21 at 22:16
  • @StephenCleary: you mean, except [this one](https://stackoverflow.com/a/16722767). Whether message pumping works or not is a separate issue; for example, the `func` delegate could be assumed to e.g. call `Application.Run()` or use some waitable object that does message pumping for the thread. Fact is, whether a thread is STA or not is _entirely_ about how COM is initialized on the thread, and not about whether that thread has an active message pump. – Peter Duniho Jul 26 '21 at 22:33
  • There is a `Task` that runs on an STA thread with message pump, in [this](https://stackoverflow.com/questions/66394112/parallel-using-of-webview2-in-async-task "Parallel using of WebView2 in async Task") question. – Theodor Zoulias Jul 27 '21 at 08:05
  • `whether a thread is STA or not is entirely about how COM is initialized on the thread, and not about whether that thread has an active message pump` Disagree, but it's not worth arguing about. – Stephen Cleary Jul 27 '21 at 12:34

1 Answers1

4

I have WinForms application, and I want to use the Windows.Forms.OpenFileDialog, which requires an STA thread.

Yes. Every WinForms app has an STA thread which is its main thread. It's easiest to just use that thread.

It works fine with threads, but what about tasks? ... How to create a task (TPL) running a STA thread?

Apartment threading models is a thread concept. There's no such thing as an STA task.

However, a Delegate Task can run on an STA thread if it is scheduled to that thread.

I don't understand how TaskScheduler.FromCurrentSynchronizationContext() is supposed to force a task to run on a STA thread

It doesn't always do that. TaskScheduler.FromCurrentSynchronizationContext will create a TaskScheduler that schedules tasks to SynchronizationContext.Current. Now, on WinForms, if the calling code is on the UI thread, then SynchronizationContext.Current is a WindowsFormsSynchronizationContext instance. That WindowsFormsSynchronizationContext will execute code on the UI thread. So, the TaskScheduler will schedule tasks to the UI thread (which is an STA thread), and the tasks end up running on the existing STA thread. Again, that is if TaskScheduler.FromCurrentSynchronizationContext was called from the UI thread in the first place.

If TaskScheduler.FromCurrentSynchronizationContext is called from a thread pool thread, then SynchronizationContext.Current is null, and the resulting TaskScheduler schedules tasks on a thread pool thread, which is not STA.

So how do I forcibly start a task on STA apartment state, so that I can use components that require it in my application?

The optimum way to do this is to structure your code so that the UI thread calls the background threads, not the other way around. Don't have your background threads call the UI to do things. If the UI is in control, then simpler patterns like await and IProgress<T> can be used to coordinate with the background threads. If background thread(s) must drive the UI, then one solution is to capture the UI SynchronizationContext (or TaskScheduler) and use that from a background thread to execute code on the UI thread.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • Note the `[STAThread]` attribute on the `Main()` in the default Windows Forms project template. I'm not sure it's required for WinForms, which is all .NET, but COM interop is a common scenario in WinForms, so it makes sense to default to STA. – David Browne - Microsoft Jul 26 '21 at 14:54
  • You specify winforms, is not the same true for WPF? – JonasH Jul 26 '21 at 15:03
  • Just as I thought I was looking at this problem from completely different (wrong) direction. Thanks for the help. – Gregor Sattel Jul 26 '21 at 15:21