Ответ 1
Иногда действительно необходимо выполнять фоновое взаимодействие с потоком пользовательского интерфейса, в частности, когда большая часть работы связана с вводом пользователя.
Пример: подсветка синтаксиса в реальном времени, как и вы. Возможно, можно разгрузить некоторые подзадачи такой фоновой операции потоку пула, но это не устранит тот факт, что текст элемента управления редактором изменяется на каждом новом типизированном символе.
Справка под рукой: await Dispatcher.Yield(DispatcherPriority.ApplicationIdle)
. Это даст пользователю входные события (мышь и клавиатура) лучшим приоритетом в цикле событий диспетчера WPF. Фоновый рабочий процесс может выглядеть следующим образом:
async Task DoUIThreadWorkAsync(CancellationToken token)
{
var i = 0;
while (true)
{
token.ThrowIfCancellationRequested();
await Dispatcher.Yield(DispatcherPriority.ApplicationIdle);
// do the UI-related work
this.TextBlock.Text = "iteration " + i++;
}
}
Это обеспечит отзывчивость пользовательского интерфейса и сделает работу фона как можно быстрее, но с приоритетом простоя.
Мы можем улучшить его с некоторым дросселем (подождите не менее 100 мс между итерациями) и лучшей логикой отмены:
async Task DoUIThreadWorkAsync(CancellationToken token)
{
Func<Task> idleYield = async () =>
await Dispatcher.Yield(DispatcherPriority.ApplicationIdle);
var cancellationTcs = new TaskCompletionSource<bool>();
using (token.Register(() =>
cancellationTcs.SetCanceled(), useSynchronizationContext: true))
{
var i = 0;
while (true)
{
await Task.Delay(100, token);
await Task.WhenAny(idleYield(), cancellationTcs.Task);
token.ThrowIfCancellationRequested();
// do the UI-related work
this.TextBlock.Text = "iteration " + i++;
}
}
}
Обновлен, поскольку OP отправил образец кода.
Основываясь на коде, который вы опубликовали, я согласен с комментарием @HighCore о правильной ViewModel.
Как вы делаете это сейчас, background.BeginInvoke
запускает фоновое действие в потоке пула, а затем синхронно вызывает поток пользовательского интерфейса в узком цикле foreach
с Dispatcher.Invoke
. Это добавляет дополнительные накладные расходы. Кроме того, вы не замечаете окончания этой операции, потому что вы просто игнорируете IAsyncResult
, возвращаемый background.BeginInvoke
. Таким образом, InitializeForm
возвращает, а background.BeginInvoke
продолжается в фоновом потоке. По сути, это вызов "огонь-и-забыть".
Если вы действительно хотите придерживаться потока пользовательского интерфейса, ниже описано, как это можно сделать с помощью описанного вами подхода.
Обратите внимание, что _initializeTask = background()
по-прежнему является асинхронной операцией, несмотря на то, что это происходит в потоке пользовательского интерфейса. Вы не сможете сделать его синхронным без вложенного цикла событий Dispatcher внутри InitializeForm
(что было бы очень плохой идеей из-за последствий с повторным подключением интерфейса).
Тем не менее, упрощенная версия (без дроссельной заслонки или отмены) может выглядеть так:
Task _initializeTask;
private void InitializeForm(List<NonDependencyObject> myCollection)
{
Action<NonDependencyObject> doWork = (nonDepObj) =>
{
var dependencyObject = CreateDependencyObject(nonDepObj);
UiComponent.Add(dependencyObject);
// Set up some binding on each dependencyObject and update progress bar
...
};
Func<Task> background = async () =>
{
foreach (var nonDependencyObject in myCollection)
{
if (nonDependencyObject.NeedsToBeAdded())
{
doWork(nonDependencyObject);
await Dispatcher.Yield(DispatcherPriority.ApplicationIdle);
}
}
};
_initializeTask = background();
}