如何在长时间的* UI *操作中让UI刷新 [英] How to let the UI refresh during a long running *UI* operation

查看:71
本文介绍了如何在长时间的* UI *操作中让UI刷新的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

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

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

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.

在这种情况下使用后台工作程序的通常解决方案不起作用,因为一旦DependencyObject由后台工作程序创建,由于它们是在后台线程上创建的,因此无法再将它们添加到UI组件中

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.

我目前对解决方案的尝试是在后台线程中运行循环,为每个工作单元分派到UI线程,然后调用Thread.Yield()使UI线程有机会进行更新.这几乎可以正常工作-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并处理其他表单上的事件?

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

根据要求,提供我当前的解决方案"的示例:

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

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

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

推荐答案

有时确实需要在UI线程上进行后台工作,尤其是在大部分工作是处理用户输入时.

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.

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

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.

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

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:

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

这将使UI保持响应状态,并尽可能快地执行后台工作,但具有空闲优先级.

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

我们可能希望通过一些节流(在两次迭代之间至少等待100毫秒)和更好的取消逻辑来增强它:

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

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的评论.

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

background.BeginInvoke当前的操作方式,background.BeginInvoke在池线程上启动后台操作,然后使用Dispatcher.Invoke在紧迫的foreach循环上同步回调UI线程.这只会增加额外的开销.此外,您不会观察到此操作的结束,因为您只是忽略了background.BeginInvoke返回的IAsyncResult.因此,InitializeForm返回,而background.BeginInvoke在后台线程上继续.本质上,这是一劳永逸的呼叫.

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.

如果您真的想坚持UI线程,下面是使用我描述的方法可以完成的操作.

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

请注意,尽管_initializeTask = background()仍发生在UI线程上,但它仍然是异步操作. 如果没有InitializeForm 内的嵌套Dispatcher事件循环,您将无法使其同步(这是一个非常糟糕的主意,因为它会影响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).

也就是说,简化版本(无节流或取消)可能看起来像这样:

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

这篇关于如何在长时间的* UI *操作中让UI刷新的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆