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

时间:2014-02-06 01:07:04

标签: c# wpf multithreading

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

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

在这种情况下使用后台工作程序的常用解决方案不起作用,因为一旦DependencyObject由后台工作程序创建,它们就不能再添加到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)似乎有效,但这真的是一个很好的解决方案吗?

1 个答案:

答案 0 :(得分:10)

有时确实需要在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 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++;
        }

    }
}

已更新,因为OP已发布示例代码。

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

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

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

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

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

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