“The calling thread must be STA” workaround

2020-02-29 03:07发布

I know there are a few answers on this topic on SO, but I can not get any of the solutions working for me. I am trying to open a new window, from an ICommand fired from within a datatemplate. Both of the following give the aforementioned error when the new window is instantiated (within "new MessageWindowP"):

Using TPL/FromCurrentSynchronizationContext Update: works

public class ChatUserCommand : ICommand
{
    public void Execute(object sender)
    {
        if (sender is UserC)
        {
            var user = (UserC)sender;
            var scheduler = TaskScheduler.FromCurrentSynchronizationContext();                  
            Task.Factory.StartNew(new Action<object>(CreateMessageWindow), user,CancellationToken.None, TaskCreationOptions.None,scheduler);         
        }
    }

    private void CreateMessageWindow(object o)
    {
        var user = (UserC)o;
        var messageP = new MessageWindowP();
        messageP.ViewModel.Participants.Add(user);
        messageP.View.Show();
    }
}

Using ThreadStart: Update: not recommended, see Jon's answer

public class ChatUserCommand : ICommand
{
    public void Execute(object sender)
    {
        if (sender is UserC)
        {
            var user = (UserC)sender;

            var t = new ParameterizedThreadStart(CreateMessageWindow);
            var thread = new Thread(t);
            thread.SetApartmentState(ApartmentState.STA);
            thread.Start(sender);           
        }
    }

    private void CreateMessageWindow(object o)
    {
        var user = (UserC)o;
        var messageP = new MessageWindowP();
        messageP.ViewModel.Participants.Add(user);
        messageP.View.Show();
    }
}

Thanks

EDIT. Based on the responses so far, I'd like to point out that I have also tried BeginInvoke on the current dispatcher, as well as executing the code in the original method (that's how the code started). See below:

BeginInvoke Update: not recommended see Jon's answer

public class ChatUserCommand : ICommand
{
    public void Execute(object sender)
    {
        if (sender is UserC)
        {
            var user = (UserC)sender;
            Dispatcher.CurrentDispatcher.BeginInvoke(new Action<object>(CreateMessageWindow), sender);       
        }
    }

    private void CreateMessageWindow(object o)
    {
        var user = (UserC)o;
        var messageP = new MessageWindowP();
        messageP.ViewModel.Participants.Add(user);
        messageP.View.Show();
    }
}

In same thread Update: works if you are on UI thread already

public class ChatUserCommand : ICommand
{
    public void Execute(object sender)
    {
        if (sender is UserC)
        {
            var user = (UserC)sender;
            var messageP = new MessageWindowP();
            messageP.ViewModel.Participants.Add(user);
            messageP.View.Show();    
        }
    }

}

BeginInvoke, using reference to dispatcher of first/main window Update: works

 public void Execute(object sender)
   {
       if (sender is UserC)
       {
            var user = (UserC)sender;
                    GeneralManager.MainDispatcher.BeginInvoke(
                               DispatcherPriority.Normal,
                               new Action(() => this.CreateMessageWindow(user)));      
        }
    }

where GeneralManager.MainDispatcher is a reference to the Dispatcher of the first window I create:

     [somewhere far far away]
        mainP = new MainP();
        MainDispatcher = mainP.View.Dispatcher;

I'm at a loss.

3条回答
爱情/是我丢掉的垃圾
2楼-- · 2020-02-29 03:49

The calling thread must not only be STA, but it must also have a message loop. There's only one thread in your application that already has a message loop, and that's your main thread. So you should use Dispatcher.BeginInvoke to open your window from your main thread.

E.g. if you have a reference to your main application window (MainWindow), you can do

MainWindow.BeginInvoke(
    DispatcherPriority.Normal, 
    new Action(() => this.CreateMessageWindow(user)));

Update: Be careful: you cannot blindly call Dispatcher.CurrentDispatcher because it doesn't do what you think it does. The documentation says that CurrentDispatcher:

Gets the Dispatcher for the thread currently executing and creates a new Dispatcher if one is not already associated with the thread.

That's why you must use the Dispatcher associated with an already-existing UI control (like your main window as in the example above).

查看更多
姐就是有狂的资本
3楼-- · 2020-02-29 03:49

With TPL you can use the StaTaskScheduler from the TPL Extras

It will run tasks on STA threads.

Only used it for COM. Never tried to run several UI threads.

查看更多
孤傲高冷的网名
4楼-- · 2020-02-29 03:49

You are trying to create a window from a background thread. You cannot do it due to a variety of reasons. Typically you need to create the window in the main application thread.

For your case, a simple idea would be just do it immediately (just call CreateMessageWindow inside Execute) instead of allocating a Task, because your command will definitely fire from the main (UI) thread. If you are unsure about the thread where your Execute runs, you can marshal it to the UI thread using Dispatcher.BeginInvoke().

There are really few cases when you would want your new window to run in a non-main thread. If this is really really needed in your case, you should add Dispatcher.Run(); after messageP.View.Show(); (using the second variant of the code). This starts the message loop in the new thread.

You shouldn't try to run window in TPL's thread, because these threads are typically threadpool threads, and therefore out of your control. For example, you cannot ensure they are STA (typically, they are MTA).

Edit:
from the error in your updated code, it's clear that the Execute runs in some non-UI thread. Try using Application.Current.Dispatcher instead of Dispatcher.CurrentDispatcher. (CurrentDispatcher means the dispatcher of the current thread, which may be wrong if the current thread is not the main one.)

查看更多
登录 后发表回答