Показать форму при завершении задачи в потоке пользовательского интерфейса

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

    private static Task RunningTask
    {
        get;
        set;
    }
    public static UpdaterTool.Forms.UpdateResult UpdateResultForm;

    private void DoWork()
    {
        UpdateResultForm = new Forms.UpdateResult(); 
        //the next line forces the creation of the handle - 
        //otherwise InvokeRequired will later on return false.
        var hackHandle = UpdateResultForm.Handle; 

        var ctx = TaskScheduler.FromCurrentSynchronizationContext();

        RunningTask = Task.Factory.StartNew(() => DownloadAndInstallFiles(), CancelTokenSource.Token)
            .ContinueWith(_ => WorkComplete(), CancelTokenSource.Token, TaskContinuationOptions.NotOnFaulted, ctx);
    }

    private void WorkComplete()
    {
       ShowResultForm();
    }

    private void ShowResultForm()
    {
        if (UpdateResultForm.InvokeRequired)
        {
            try
            {
                UpdateResultForm.Invoke(new MethodInvoker(ShowResultForm));
            }
            catch { }
            return;
         }
         UpdateResultForm.Show();

     }

Проблема в том, что независимо от того, какую комбинацию перегрузок для ContinueWith() я использую, UpdateResultForm либо вообще не отображается (имеется в виду продолжение не происходит, воркер зависает на "запуске"), либо когда есть, то зависает пользовательский интерфейс, например, ожидание завершения рабочего потока или что-то в этом роде. Я не понимаю, почему это происходит, когда я пытался показать это в потоке пользовательского интерфейса, используя FromCurrentSynchronizationContext().

В моем понимании, внутри метода DoWork я запускаю UI-поток (поэтому я инициализирую форму именно там). Когда код входит в Task.Factory.StartNew, он переключается на рабочий поток. Когда он завершается, он продолжает работу с WorkComplete, который просто показывает ранее инициализированную форму в потоке пользовательского интерфейса.

Что мне не хватает? Спасибо,


person Amc_rtty    schedule 21.07.2012    source источник
comment
Как вы называете DoWork()? Вы вызываете его из потока пользовательского интерфейса?   -  person svick    schedule 21.07.2012
comment
Я думаю, что да, он вызывается из потока пользовательского интерфейса. Код будет следовать в комментарии, извините, не могу отформатировать его: При событиях (запуск приложения и нажатие кнопки) я делаю следующее: UpdatesManager manager = new UpdatesManager();manager.PerformUpdate(); - затем метод PerformUpdate() делает некоторые проверки и вызывает DoWork().   -  person Amc_rtty    schedule 21.07.2012
comment
FWIW, BackgroundWorker автоматически запускает прогресс и завершенные делегаты в потоке пользовательского интерфейса для вас.   -  person James Manning    schedule 21.07.2012
comment
В WinForms обычно используется шаблон InvokeRequired/Invoke — см. msdn.microsoft. com/en-us/library/ms171728.aspx   -  person James Manning    schedule 21.07.2012
comment
Ссылка на BackgroundWorker (не позволяет мне редактировать комментарий выше, так как прошло более 5 минут) здесь: msdn.microsoft.com/en-us/library/   -  person James Manning    schedule 21.07.2012
comment
Спасибо, но я переключился с BW на использование Задач, так что о возвращении к BW не может быть и речи. И я уже использую шаблон InvokeRequired — см. содержимое метода ShowResultForm(). Спасибо за ваше время   -  person Amc_rtty    schedule 21.07.2012


Ответы (4)


Использование InvokeRequired — сильный антишаблон. Не так уж часто бывает, что вы не знаете, в каком потоке выполняется метод. То же самое и здесь: вы уже использовали TaskScheduler.FromCurrentSynchronizationContext(), поэтому вы знаете, что задача выполняется в потоке пользовательского интерфейса. Нет смысла проверять еще раз. Что также избавляет от необходимости создавать дескриптор формы заранее.

Проблемы, с которыми вы сталкиваетесь, могут быть вызваны запуском DoWork() из рабочего потока. Возможно, вы также вызвали его из задачи. Что заставляет FromCurrentSynchronizationContext() возвращать неправильный контекст. И будет отображать форму в потоке, который не прокачивает цикл сообщений, он будет мертв, как дверной гвоздь. Или заблокировав поток пользовательского интерфейса, ожидая завершения задачи. Это всегда будет вызывать взаимоблокировку, задача не может быть завершена, если поток пользовательского интерфейса не будет бездействовать и не сможет выполнить метод ShowResultForm().

person Hans Passant    schedule 21.07.2012
comment
Спасибо за ваш ответ, он помог мне лучше понять проблемы в моем коде. Ваше предположение верно - я использовал проверку потоков и установил точку останова при входе в метод DoWork - когда выполнение входит в этот метод, текущий поток ЯВЛЯЕТСЯ рабочим. Чего я не понимаю, так это почему на земле это происходит. Возможно, это связано с тем, что при запуске приложения winform не отображается (только значок на панели задач), и оно немедленно вызывает DoWork. Также в этом приложении есть только одна Задача, и она создается внутри DoWork. Сам DoWork не вызывается родительской задачей. - person Amc_rtty; 21.07.2012

Я думаю, что решение этой проблемы таково: вызывается ли DoWork() в любом потоке, кроме потока пользовательского интерфейса?

Если да, рассмотрите возможность создания экземпляра UpdateResultForm где-нибудь в потоке пользовательского интерфейса или, если это невозможно, то где-нибудь в вашей ContinueWith операции.

Если нет, то проблем быть не должно. UpdateResultForm.Handle мог вызвать некоторые проблемы. Однако в этом случае этот дескриптор больше не нужен, так как вы уже находитесь в потоке пользовательского интерфейса и, следовательно, вам не нужно проверять, требуется ли Invoke или нет.

В любом из обоих случаев вы можете попробовать переписать свой метод DoWork следующим образом:

     private void DoWork()
            {
                var ctx = TaskScheduler.FromCurrentSynchronizationContext();

                RunningTask = Task.Factory
                    .StartNew(DownloadAndInstallFiles, CancelTokenSource.Token)
                    .ContinueWith(_ => ShowResultForm(), CancelTokenSource.Token, TaskContinuationOptions.NotOnFaulted, ctx);
            }

            private void ShowResultForm()
            {
                UpdateResultForm = new Forms.UpdateResult();
                // Some other code
                UpdateResultForm.Show();
            }

Даже WorkComplete больше не требуется, так как он просто передает вызов. Надеюсь, что это поможет вам.

person Oliver Vogel    schedule 21.07.2012

Если вы используете SynchronizationContext, вам не нужен InvokeRequired/Invoke. Единственный способ столкнуться с проблемой, которую вы видите здесь, — это если DoWork вызывается в потоке, отличном от потока пользовательского интерфейса.

например если я возьму предоставленный вами код, создам свою собственную форму UpdateResult, добавлю элемент CancelTokenSource и инициализирую его в конструкторе, а также запущу DoWork из потока пользовательского интерфейса, он будет работать нормально.

person Peter Ritchie    schedule 21.07.2012
comment
Вы правы, я только что обнаружил, что при входе в DoWork текущий поток был рабочим. Итак, теперь вопрос, как я могу быть уверен, что вызываю DoWork из потока пользовательского интерфейса, учитывая, что мое приложение при запуске не имеет формы, а только значок в системном трее, и оно немедленно запускает процедуру обновления (которая, как я вижу в окне проверки потоков создает рабочие потоки; эти рабочие потоки создаются приложением, а не инициализируются мной явно)? Спасибо. - person Amc_rtty; 21.07.2012
comment
Ну, честно говоря, вы пишете код для вызова DoWork, просто вызывайте его правильно. Добавление чего-то в DoWork, чтобы убедиться, что он вызывается из потока пользовательского интерфейса, кажется, не добавляет большой ценности, когда вы можете просто правильно вызвать DoWork, что я бы и рекомендовал. - person Peter Ritchie; 21.07.2012

Я решил это. Все ответы в этой ветке верны, проблема здесь, как и предположил Ганс, заключалась в том, что FromCurrentSynchronizationContext() возвращал неправильный контекст вместо ожидаемого пользовательского интерфейса. Это произошло из-за того, что DoWork вызывался из рабочего потока, а не из UI.

Решение здесь, в моем конкретном случае, состояло в том, чтобы вызвать DoWork из контекста пользовательского интерфейса, что означает, что мне пришлось создать объект WindowsFormsSynchronizationContext точно так же, как этот пост объясняет.

person Amc_rtty    schedule 22.07.2012