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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/21592036/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-13 10:44:10  来源:igfitidea点击:

How to let the UI refresh during a long running *UI* operation

c#wpfmultithreading

提问by Taedrin

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 mustrun 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 线程上运行。基本上,我DependencyObject在运行时动态构造s 并将它们添加到我的 WPF 应用程序的 UI 组件中。DependencyObject需要创建的s的数量取决于用户输入,没有限制。我拥有的其中一个测试输入DependencyObject需要创建大约 6000秒,加载它们需要几分钟。

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)似乎有效,但这真的是一个好的解决方案吗?

采纳答案by noseratio

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:

手头的帮助:await 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 毫秒)和更好的取消逻辑来增强它:

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.BeginInvokestarts a background operation on a pool thread, then synchronously calls back the UI thread on a tight foreachloop, 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 IAsyncResultreturned by background.BeginInvoke. Thus, InitializeFormreturns, while background.BeginInvokecontinues on a background thread. Essentially, this is a fire-and-forget call.

您目前的做法是background.BeginInvoke在池线程上启动后台操作,然后在紧密foreach循环中同步回调 UI 线程,使用Dispatcher.Invoke. 这只会增加额外的开销。此外,您没有观察到此操作的结束,因为您只是忽略了IAsyncResult返回的background.BeginInvoke. 因此,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 线程上。如果内部没有嵌套的 Dispatcher 事件循环,您将无法使其同步InitializeForm(由于 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();
}