【问题标题】:How to let the UI refresh during a long running *UI* operation如何在长时间运行 *UI* 操作期间让 UI 刷新
【发布时间】:2014-03-02 18:09:51
【问题描述】:

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

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

在这种情况下使用后台worker的通常解决方案是行不通的,因为一旦DependencyObjects被后台worker创建,它们就不能再添加到UI组件中,因为它们是在后台线程上创建的.

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

如何让我的应用程序在这个长时间运行的操作期间不断更新 UI 和处理其他表单上的事件?

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

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

【问题讨论】:

  • 发布您的代码示例。
  • 似乎主要问题是您需要创建的 DependencyObject 的数量。在这种情况下,您可以尝试将创建分解为您在调度程序上排队的多个任务。只要单个 DependencyObject 的创建运行时间不会太长,您就可以分块处理它们,并且仍然保持 UI 相当灵敏。 “如果我们将计算任务分解为可管理的块,我们可以定期返回 Dispatcher 并处理事件。我们可以让 WPF 有机会重新绘制和处理输入。” - msdn.microsoft.com/en-us/library/ms741870(v=vs.110).aspx
  • Basically, I am dynamically constructing DependencyObjects at run time and adding them to a UI component on my WPF application - 我一直在想,如果你真正需要的是一个 ItemsControl 和一个合适的 ViewModel。
  • 根据您发布的代码,我同意@HighCore。但是,如果您想坚持 UI 线程,我已将更新发布到我的 answer。请注意,_initializeTask = background() 是一个异步操作,尽管发生在 UI 线程上。如果没有模式 Dispatcher 消息循环,您将无法使其同步(这将是一个非常糟糕的主意)。
  • @Noseratio - 不幸的是,我认为 ItemsControl 不会帮助我(尽管我敢打赌,在 MVVM 方面的经验会对我有很大帮助)。 DependencyObject 不是 UIElement,也不是 UI 的直接部分。它们只是向 UI 组件提供数据。我所做的实际上是从一个或多个任意数据库查询中获取记录,并将它们显示为地图上的点。

标签: c# wpf multithreading


【解决方案1】:

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

示例:实时语法高亮显示,随你输入。可以将此类后台操作的一些子工作项卸载到池线程,但这并不能消除编辑器控件的文本随着每个新键入的字符而变化的事实。

帮助: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++;
    }
}

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

我们可能希望通过一些限制(在迭代之间等待至少 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 循环上同步回调 UI 线程,并使用Dispatcher.Invoke。这只会增加额外的开销。此外,您没有观察到此操作的结束,因为您只是忽略了background.BeginInvoke 返回的IAsyncResult。因此,InitializeForm 返回,而background.BeginInvoke 在后台线程上继续。本质上,这是一个即发即弃的调用。

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

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

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

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

【讨论】:

  • 感谢所有帮助。明天早上上班时我会试一试,如果对我有用,请标记已接受的答案。
  • 我还应该提到我想要一个即发即弃的异步操作。虽然在后台初始化完成之前禁用表单,但我希望应用程序中的其他表单能够响应。
  • @user1512185,所以你并不关心后台操作什么时候真正结束,并且所有dependencyObjects都被添加了?或者在课程中是否有任何例外?这就是“一劳永逸”。
  • 好吧,我关心异常,但我认为这些可以在后台操作中处理,不是吗?但我不关心操作何时结束,因为操作设置了一个属性,该属性通过绑定启用 UI。
  • @Pangamma 上次我进行任何广泛的 WPF 开发时,它工作得很好,大约在这个答案的时候。实际的解决方案比这个更复杂。它正在使用GetQueueStatus Win32 API 检查 UI 线程的输入队列的状态,并在看到任何待处理的输入时立即让步,以避免鼠标和键盘处理中的任何延迟。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-03-17
  • 2010-11-16
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多