Вызывающий поток должен быть обходным путем STA

Я знаю, что есть несколько ответов по этой теме на SO, но я не могу заставить ни одно из решений работать на меня. Я пытаюсь открыть новое окно из команды ICommand, запущенной из таблички данных. Оба следующих события вызывают вышеупомянутую ошибку при создании экземпляра нового окна (в пределах "new MessageWindowP"):

Использование TPL / FromCurrentSynchronizationContext Обновление: работает

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();
    }
}

Использование ThreadStart: Обновление: не рекомендуется, см. ответ Джона

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();
    }
}

Спасибо

РЕДАКТИРОВАТЬ. Основываясь на полученных ответах, я хотел бы указать, что я также пробовал BeginInvoke в текущем диспетчере, а также выполнял код в исходном методе (так код запускался). См. ниже:

BeginInvoke Обновление: не рекомендуется см. ответ Джона

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();
    }
}

В том же потоке Обновление: работает, если вы уже находитесь в потоке пользовательского интерфейса

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, используя ссылку на диспетчер первого / главного окна Обновление: работает

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

где GeneralManager.MainDispatcher - ссылка на Диспетчер первого создаваемого мной окна:

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

Я в растерянности.


person Harry Mexican    schedule 26.04.2012    source источник
comment
А в чем была проблема с вызовом в том же потоке и BeginInvoke? В каком потоке работает ваш Execute?   -  person Vlad    schedule 26.04.2012
comment
Вы не можете использовать Dispatcher.CurrentDispatcher, как здесь. Смотрите обновление моего ответа.   -  person Jon    schedule 26.04.2012
comment
Ребята. Все равно не повезло ... :( см. Обновление еще раз.   -  person Harry Mexican    schedule 26.04.2012
comment
Не могли бы вы опубликовать короткий, но полный код, который мы могли бы использовать для воспроизведения вашей проблемы?   -  person svick    schedule 26.04.2012
comment
ОК, свик. Я постараюсь вырвать код из кодовой базы и создать минимально воспроизводимый проект.   -  person Harry Mexican    schedule 27.04.2012
comment
МОЙ БОГ. Извините за зря потраченное время, ребята. Оказывается, все эти варианты работают нормально. Просто так случилось, что окно также создавалось из другого места, которое не было безопасным для пользовательского интерфейса. Я не мог сказать, потому что поток, который я смотрел в отладчике, и параллельный, шли в идеальной синхронизации !! Проголосую за вас всех, и, надеюсь, это все еще будет полезно для людей, которые хотят видеть различные параметры диспетчера.   -  person Harry Mexican    schedule 28.04.2012


Ответы (3)


Вызывающий поток не должен только быть STA, но он также должен иметь цикл сообщений. В вашем приложении есть только один поток, в котором уже есть цикл сообщений, и это ваш основной поток. Таким образом, вы должны использовать Dispatcher.BeginInvoke, чтобы открыть окно из основного потока.

Например. если у вас есть ссылка на главное окно приложения (MainWindow), вы можете сделать

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

Обновление: Будьте осторожны: вы не можете слепо позвонить Dispatcher.CurrentDispatcher, потому что он не делает того, что вы думаете. В документации говорится, что CurrentDispatcher:

Получает Dispatcher для потока, выполняемого в данный момент, и создает новый Dispatcher, если он еще не связан с потоком.

Вот почему вы должны использовать Dispatcher, связанный с уже существующим элементом управления пользовательского интерфейса (например, с вашим главным окном, как в примере выше).

person Jon    schedule 26.04.2012
comment
Есть другие идеи, Джон? Я пробовал использовать Диспетчер своего главного окна. - person Harry Mexican; 26.04.2012


Вы пытаетесь создать окно из фонового потока. Вы не можете этого сделать по разным причинам. Обычно вам нужно создать окно в основном потоке приложения.

В вашем случае простая идея - просто сделать это немедленно (просто вызвать CreateMessageWindow внутри Execute) вместо выделения Task, потому что ваша команда определенно будет запускаться из основного (UI) потока. Если вы не уверены в потоке, в котором работает ваш Execute, вы можете маршалировать его в поток пользовательского интерфейса, используя Dispatcher.BeginInvoke().

На самом деле очень мало случаев, когда вы хотите, чтобы ваше новое окно работало в неосновном потоке. Если в вашем случае это действительно действительно необходимо, вы должны добавить Dispatcher.Run(); после messageP.View.Show(); (используя второй вариант кода). Это запускает цикл сообщений в новом потоке.

Вы не должны пытаться запустить окно в потоке TPL, потому что эти потоки обычно являются потоками пула потоков и, следовательно, находятся вне вашего контроля. Например, вы не можете гарантировать, что они являются STA (обычно это MTA).

Изменить:
из ошибки в обновленном коде ясно, что Execute работает в каком-то потоке, отличном от пользовательского интерфейса. Попробуйте использовать Application.Current.Dispatcher вместо Dispatcher.CurrentDispatcher. (CurrentDispatcher означает диспетчер текущего потока, что может быть неправильным, если текущий поток не является основным.)

person Vlad    schedule 26.04.2012
comment
Всем привет. Спасибо за вашу помощь. Все, что вы говорите, имеет смысл, но я прибегал к этим методам только потому, что запуск кода внутри Execute вызывал ту же ошибку. См. Обновленный вопрос, чтобы узнать о других вещах, которые я пробовал. - person Harry Mexican; 26.04.2012