Как разрешить обновлять пользовательский интерфейс во время длительной работы * UI *

Прежде чем вы ставите мой вопрос как дубликат, выслушайте меня.

У большинства людей есть длительная работа, отличная от UI, которую они выполняют, и нужно разблокировать поток пользовательского интерфейса. У меня длинная работа UI, которая должна запускаться в потоке пользовательского интерфейса, который блокирует остальную часть моего приложения. В принципе, я динамически строю DependencyObject во время выполнения и добавляю их к компоненту пользовательского интерфейса в своем приложении WPF. Количество DependencyObject, которое необходимо создать, зависит от пользовательского ввода, которого нет предела. Один из тестовых входов, которые у меня есть, составляет около 6000 DependencyObject, которые необходимо создать, и загрузка их занимает пару минут.

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

Моя текущая попытка решения - запустить цикл в фоновом потоке, отправить в поток пользовательского интерфейса для каждой единицы работы, а затем вызвать Thread.Yield(), чтобы дать потоку пользовательского интерфейса возможность обновиться. Это почти работает - поток пользовательского интерфейса получает возможность обновить себя пару раз во время операции, но приложение по-прежнему по-прежнему заблокировано.

Как я могу заставить приложение продолжать обновлять пользовательский интерфейс и обрабатывать события в других формах во время этой длительной операции?

EDIT: В соответствии с запросом, пример моего текущего "решения":

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
            ...
        };

    Action background = () =>
        {
            foreach (var nonDependencyObject in myCollection)
            {
                 if (nonDependencyObject.NeedsToBeAdded())
                 {
                     Dispatcher.Invoke(doWork, nonDependencyObject);
                     Thread.Yield();  //Doesn't give UI enough time to update
                 }
            }
        };
    background.BeginInvoke(background.EndInvoke, null);
}

Изменение Thread.Yield() до Thread.Sleep(1) похоже на работу, но действительно ли это хорошее решение?

Ответ 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();
}