Как реализовать шаблон пользовательского интерфейса InvokeRequired внутри асинхронного метода?

Допустим, у меня есть Form который пытается справиться с многопоточной средой; поэтому он проверяет, работает ли он в потоке пользовательского интерфейса, прежде чем будет произведена какая-либо модификация пользовательского интерфейса:

partial class SomeForm : Form
{
    public void DoSomethingToUserInterface()
    {
        if (InvokeRequired)
        {
            BeginInvoke(delegate { DoSomethingToUserInterface() });
        }
        else
        {
            … // do the actual work (e.g. manipulate the form or its elements)
        }
    }
}

Теперь допустим, что я выполняю длительную операцию внутри часть этого метода; Поэтому я хотел бы сделать это асинхронным с помощью async/await,

Учитывая, что я должен изменить сигнатуру метода, чтобы вернуть Task вместо void (так что исключения могут быть перехвачены), как бы я реализовать часть, которая выполняет BeginInvoke? Что он должен вернуть?

public async Task DoSomethingToUserInterfaceAsync()
{
    if (InvokeRequired)
    {
        // what do I put here?
    }
    {
        … // like before (but can now use `await` expressions)
    }
}

2 ответа

Решение

Когда используешь async-await с обычаем awaiter такой как общий TaskAwaiter, SynchronizationContext скрытно захвачен для вас. Позже, когда асинхронный метод завершается, продолжение (любой код после await) возвращается в тот же контекст синхронизации с помощью SynchronizationContext.Post,

Это в целом исключает необходимость использования InvokeRequired и другие методы, используемые для основной работы в потоке пользовательского интерфейса. Чтобы сделать это, вам нужно будет отследить все вызовы методов до вызовов верхнего уровня и реорганизовать их для использования. async-await наверное.

Но для решения конкретной проблемы, как вы можете сделать, это захватить WinFormSynchronizationContext Когда ваш Form инициализирует:

partial class SomeForm : Form
{
    private TaskScheduler _uiTaskScheduler;
    public SomeForm()
    {
        _uiTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
    }
}

А потом используйте его, когда хотите await:

if (InvokeRequired)
{
    Task uiTask = new Task(() => DoSomethingToUserInterface());
    uiTask.RunSynchronously(_uiTaskScheduler);
}
else
{
    // Do async work
}

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

Затем, когда вы заинтересованы в запуске любой задачи в потоке пользовательского интерфейса, вы должны передать uiScheduler к методу StartNew, так что TPL будет планировать задачу в предоставленном планировщике (поток пользовательского интерфейса в этом случае).

В любом случае, вы решили запустить материал в потоке пользовательского интерфейса, поэтому просто запланируйте его на UIScheduler, вам не нужно проверять InvokeRequired бы то ни было.

public async Task DoSomethingToUserInterfaceAsync()
{
    await Task.Factory.StartNew(() => DoSomethingToUserInterface(), CancellationToken.None, TaskCreationOptions.None, uiScheduler);
    ...
}

Чтобы получить планировщик пользовательского интерфейса, вы можете использовать следующий код

private TaskScheduler uiScheduler = TaskScheduler.FromCurrentSynchronizationContext();

Примечание: крайне важно, чтобы TaskScheduler.FromCurrentSynchronizationContext должен вызываться только из потока пользовательского интерфейса, в противном случае он выдаст исключение, или вы получите TaskScheduler для некоторых других SynchronizationContext который не будет делать то, что вам нужно.

Также обратите внимание, что если вы запустили асинхронную операцию из самого потока пользовательского интерфейса, вам не понадобится какое-либо из вышеперечисленного. Ожидание возобновится в контексте, где оно было начато.

Другие вопросы по тегам