如何在长时间运行* UI *操作期间刷新UI

时间:2021-12-28 21:01:48

Before you flag my question as being a duplicate, hear me out.

在你将我的问题标记为重复之前,请听我说。

Most people have a long running non-UI operation that they are doing and need to unblock the UI thread. I have a long running UI operation which must run on the UI thread which is blocking the rest of my application. Basically, I am dynamically constructing DependencyObjects at run time and adding them to a UI component on my WPF application. The number of DependencyObjects that need to be created depends upon user input, of which there is no limit. One of the test inputs I have has about 6000 DependencyObjects that need to be created and loading them takes a couple minutes.

大多数人都在进行长时间运行的非UI操作,并且需要取消阻止UI线程。我有一个长期运行的UI操作,必须在UI线程上运行,这阻止了我的应用程序的其余部分。基本上,我在运行时动态构造DependencyObjects并将它们添加到我的WPF应用程序上的UI组件。需要创建的DependencyObject的数量取决于用户输入,其中没有限制。我有一个测试输入有大约6000个DependencyObjects需要创建并加载它们需要几分钟。

The usual solution of using a background worker in this case does not work, because once the DependencyObjects are created by the background worker, they can no longer be added to the UI component since they were created on the background thread.

在这种情况下使用后台工作程序的常用解决方案不起作用,因为一旦DependencyObjects由后台工作程序创建,它们就不能再添加到UI组件,因为它们是在后台线程上创建的。

My current attempt at a solution is to run the loop in a background thread, dispatch to the UI thread for each unit of work and then calling Thread.Yield() to give the UI thread a chance to update. This almost works - the UI thread does get the chance to update itself a couple times during the operation, but the application is still essentially blocked.

我目前尝试解决方案是在后台线程中运行循环,为每个工作单元分配到UI线程,然后调用Thread.Yield()以给UI线程提供更新的机会。这几乎可以工作 - UI线程确实有机会在操作期间多次更新自己,但应用程序仍然基本上被阻止。

How can I get my application to keep updating the UI and processing events on other forms during this long running operation?

在长时间运行的操作过程中,如何让我的应用程序继续更新UI并在其他表单上处理事件?

EDIT: As requested, an example of my current 'solution':

编辑:根据要求,我当前的“解决方案”的一个例子:

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

Changing Thread.Yield() to Thread.Sleep(1) seems to work, but is that really a good solution?

将Thread.Yield()更改为Thread.Sleep(1)似乎可行,但这真的是一个很好的解决方案吗?

1 个解决方案

#1


10  

Sometimes it is indeed required to do the background work on the UI thread, particularly, when the majority of work is to deal with the user input.

有时确实需要在UI线程上进行后台工作,特别是当大多数工作是处理用户输入时。

Example: real-time syntax highlighting, as-you-type. It might be possible to offload some sub-work-items of such background operation to a pool thread, but that wouldn't eliminate the fact the text of the editor control is changing upon every new typed character.

示例:实时语法突出显示,即按型。可以将这种后台操作的一些子工作项卸载到池线程,但这并不能消除编辑器控件的文本在每个新键入的字符上发生变化的事实。

Help at hand: await Dispatcher.Yield(DispatcherPriority.ApplicationIdle). This will give the user input events (mouse and keyboard) the best priority on the WPF Dispatcher event loop. The background work process may look like this:

手头的帮助:等待Dispatcher.Yield(DispatcherPriority.ApplicationIdle)。这将为用户输入事件(鼠标和键盘)提供WPF Dispatcher事件循环的最佳优先级。后台工作流程可能如下所示:

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

This will keep the UI responsive and will do the background work as fast as possible, but with the idle priority.

这将使UI保持响应,并将尽可能快地完成后台工作,但具有空闲优先级。

We may want to enhance it with some throttle (wait for at least 100 ms between iterations) and better cancellation logic:

我们可能希望通过一些限制来增强它(在迭代之间等待至少100 ms)和更好的取消逻辑:

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

    }
}

Updated as the OP has posted a sample code.

在OP发布示例代码时更新。

Based upon the code you posted, I agree with @HighCore's comment about the proper ViewModel.

根据您发布的代码,我同意@ HighCore关于正确ViewModel的评论。

The way you're doing it currently, background.BeginInvoke starts a background operation on a pool thread, then synchronously calls back the UI thread on a tight foreach loop, with Dispatcher.Invoke. This only adds an extra overhead. Besides, you're not observing the end of this operation, because you're simply ignoring the IAsyncResult returned by background.BeginInvoke. Thus, InitializeForm returns, while background.BeginInvoke continues on a background thread. Essentially, this is a fire-and-forget call.

你当前正在这样做的方式,background.BeginInvoke在池线程上启动后台操作,然后使用Dispatcher.Invoke在紧密的foreach循环上同步回调UI线程。这只会增加额外的开销。此外,您没有观察到此操作的结束,因为您只是忽略background.BeginInvoke返回的IAsyncResult。因此,InitializeForm返回,而background.BeginInvoke在后台线程上继续。从本质上讲,这是一个即发即弃的电话。

If you really want to stick to the UI thread, below is how it can be done using the approach I described.

如果你真的想坚持使用UI线程,下面是使用我描述的方法完成它的方法。

Note that _initializeTask = background() is still an asynchronous operation, despite it's taking place on the UI thread. You won't be able to make it synchronous without a nested Dispatcher event loop inside InitializeForm (which would be a really bad idea because of the implications with the UI re-entrancy).

请注意,_initializeTask = background()仍然是一个异步操作,尽管它发生在UI线程上。如果没有在InitializeForm中嵌套的Dispatcher事件循环,你将无法使它同步(由于UI重新入侵的影响,这将是一个非常糟糕的主意)。

That said, a simplified version (no throttle or cancellation) may look like this:

也就是说,简化版(无油门或取消)可能如下所示:

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

#1


10  

Sometimes it is indeed required to do the background work on the UI thread, particularly, when the majority of work is to deal with the user input.

有时确实需要在UI线程上进行后台工作,特别是当大多数工作是处理用户输入时。

Example: real-time syntax highlighting, as-you-type. It might be possible to offload some sub-work-items of such background operation to a pool thread, but that wouldn't eliminate the fact the text of the editor control is changing upon every new typed character.

示例:实时语法突出显示,即按型。可以将这种后台操作的一些子工作项卸载到池线程,但这并不能消除编辑器控件的文本在每个新键入的字符上发生变化的事实。

Help at hand: await Dispatcher.Yield(DispatcherPriority.ApplicationIdle). This will give the user input events (mouse and keyboard) the best priority on the WPF Dispatcher event loop. The background work process may look like this:

手头的帮助:等待Dispatcher.Yield(DispatcherPriority.ApplicationIdle)。这将为用户输入事件(鼠标和键盘)提供WPF Dispatcher事件循环的最佳优先级。后台工作流程可能如下所示:

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

This will keep the UI responsive and will do the background work as fast as possible, but with the idle priority.

这将使UI保持响应,并将尽可能快地完成后台工作,但具有空闲优先级。

We may want to enhance it with some throttle (wait for at least 100 ms between iterations) and better cancellation logic:

我们可能希望通过一些限制来增强它(在迭代之间等待至少100 ms)和更好的取消逻辑:

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

    }
}

Updated as the OP has posted a sample code.

在OP发布示例代码时更新。

Based upon the code you posted, I agree with @HighCore's comment about the proper ViewModel.

根据您发布的代码,我同意@ HighCore关于正确ViewModel的评论。

The way you're doing it currently, background.BeginInvoke starts a background operation on a pool thread, then synchronously calls back the UI thread on a tight foreach loop, with Dispatcher.Invoke. This only adds an extra overhead. Besides, you're not observing the end of this operation, because you're simply ignoring the IAsyncResult returned by background.BeginInvoke. Thus, InitializeForm returns, while background.BeginInvoke continues on a background thread. Essentially, this is a fire-and-forget call.

你当前正在这样做的方式,background.BeginInvoke在池线程上启动后台操作,然后使用Dispatcher.Invoke在紧密的foreach循环上同步回调UI线程。这只会增加额外的开销。此外,您没有观察到此操作的结束,因为您只是忽略background.BeginInvoke返回的IAsyncResult。因此,InitializeForm返回,而background.BeginInvoke在后台线程上继续。从本质上讲,这是一个即发即弃的电话。

If you really want to stick to the UI thread, below is how it can be done using the approach I described.

如果你真的想坚持使用UI线程,下面是使用我描述的方法完成它的方法。

Note that _initializeTask = background() is still an asynchronous operation, despite it's taking place on the UI thread. You won't be able to make it synchronous without a nested Dispatcher event loop inside InitializeForm (which would be a really bad idea because of the implications with the UI re-entrancy).

请注意,_initializeTask = background()仍然是一个异步操作,尽管它发生在UI线程上。如果没有在InitializeForm中嵌套的Dispatcher事件循环,你将无法使它同步(由于UI重新入侵的影响,这将是一个非常糟糕的主意)。

That said, a simplified version (no throttle or cancellation) may look like this:

也就是说,简化版(无油门或取消)可能如下所示:

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