【问题标题】:Threading and WPF's Binding线程和 WPF 的绑定
【发布时间】:2011-12-27 14:52:01
【问题描述】:

情况

我的应用程序出现以下不稳定行为:大约 20 次执行中的一次,绑定到 DataTable 的 WPFToolkit 的 DataGrid 不会呈现所有行,缺少 1 到 3 之间的任何内容预期的整 4 行。

内部运作

  • DataGrid 绑定到 DataTableD1,这是自定义类 C1 的属性。
  • 当用户刺激视图时,我们必须从后端检索数据,这可能需要一些时间。为此,我们创建了一个线程(实际上,我们使用BackgroundWorker,但似乎与使用其中一个没有区别),它运行一个方法,M1,打开连接并请求数据。该线程用于避免应用程序无响应。
  • M1 首先检索数据并将其存储在 DTO 上。之后,他要求 C1 清理它的桌子。 C1 这样做(通过调用 D1.Clear())并引发 NotifyPropertyChanged()(来自线程)。
  • M1 将新后端的DataTable 传递给C1,后者将逐行插入D1。完成插入行后,C1 引发NotifyPropertyChanged()。线程退出。

也就是说,我清表,通知WPF,插入数据,通知WPF,然后退出。

在我看来,只要从 UI 正确使用最后一个通知,它就应该始终显示所有行。

除了DataTable,还有大量属性(主要是字符串和int)正在更新并因此得到通知。我们没有在任何其他情况下观察到这种行为,只有DataTable

我知道这深入到 WPF 绑定机制中,但我希望任何人都可以在这里阐明。欢迎任何关于 WPF 绑定或 WPF 多线程的信息。

【问题讨论】:

  • 当 M1 要求 C1 对 D1 进行更改时,您是否正在调度到 UI 线程?
  • @KentBoogaart:不!更改是在线程本身中进行的。但是,在进行更改后,C1 会引发 NotifyPropertyChanged。我相信这应该足以保证 UI 将使用正确的内容进行刷新。是真的吗?

标签: wpf multithreading wpftoolkit


【解决方案1】:

DataTable 早于 WPF,因此不实现 INotifyCollectionChanged,这是 WPF 监视集合更改的方式。你有两个选择:

  1. 用新的 DataTable 替换现有的 DataTable(在设置行之后)。然后触发属性更改通知。
  2. 从 DataTable 更改为 ObservableCollection。每当您更改项目列表时,该集合都会触发更改通知。 (请注意,如果您更改列表中已有项目之一的内容,它不会触发)

INotifyPropertyChanged 会在属性更改时通知,而不是在内部状态(无论是属性还是集合)更改时通知。当您触发 Property Changed 事件时,如果该属性与上次绑定数据时的对象不同,则 WPF 只会重新绑定控件。当您只更改对象图中的一个属性向下几层时,这可以防止它刷新整个屏幕。

【讨论】:

  • David,我只是在 加载了行之后才通知我。我不需要在添加过程中通知屏幕,因为应用程序只从用户刺激中检索数据。无论如何,您说 WPF 仅在认为数据不同时才更新屏幕。他是怎么做到的?
  • @BrunoBrant,WPF 通过保留对已绑定数据的引用并将其与新引用进行比较来了解数据是否不同。你可以阅读更多关于它的信息herehere。还有一个选项我忘了提。在触发第一个 PropertyChanged 通知之前将实际属性设置为 null,然后在触发最后一个通知之前将其设置回您的数据表。
  • 但我没有更改 DataContext,我只是更新了对象的一个​​属性。 Equal() 是否仍用于确定更改?我的 DataContext 不是 DataTable,而是一个具有 DataTable 属性的对象。
  • @BrunoBrant DataContext 也是一个属性。该检查适用于所有绑定,因为它使用反射并且是一项昂贵的操作,尤其是在数据集合的情况下。
【解决方案2】:

您是否将新数据加载到已绑定到 DataGrid 的 相同 DataTable 实例中?

如果是这样,那么 (a) 每次您从后台代码更改 DataTable 时,它​​都会从错误的线程触发通知,这是不允许的; (b) 当您最后触发 PropertyChanged 时,DataGrid 可能足够聪明地注意到引用实际上并没有改变,因此它不需要做任何事情。 (我不知道 DataGrid 是否试图变得那么聪明,但这并不是不合理的——尤其是考虑到 WPF constructs views on top of collections 的方式——它可能有助于解释你所看到的症状。)

尝试在每次需要刷新时创建一个新的 DataTable 实例,然后在从后台线程填充完该实例后,将新的(完全填充的)引用分配给通知属性并触发 PropertyChanged(以及,当然,请确保从 UI 线程执行 assignment+PropertyChanged)。

【讨论】:

  • Joe,为什么我需要从 UI 线程执行此操作? AFAIC 的 PropertyChanged 已经使用调度程序来执行此操作...
  • @BrunoBrant,PropertyChanged 通知似乎确实跨线程编组,但我实际上还没有看到文档说它是有保证的,而且我已经看到了自己执行跨线程通知的 MVVM 框架。所以我不确定该相信多少。如果您看到奇怪的行为,我倾向于从消除所有未知数开始。
  • 感谢您的提示。我现在正在编组它,它似乎能够解决问题......但到目前为止,我只用 Invoke 测试它,而不是 BeginInvoke,这让我怀疑,也许通过使用调用,我最小化了发生的错误...
【解决方案3】:

基于 Asti 的第三点,我经常遇到一个跨线程的 PropertyChanged 场景,并为此提供了一个基本视图模型。视图模型基于 PRISM NotificationObject,当然如果您不想使用 PRISM,您可以直接实现 INotifyPropertyChanged 接口。如果您曾经使用过 Silverlight,它同样适用。

namespace WPF.ViewModel
{
    using System.Windows;
    using System.Windows.Threading;

    using Microsoft.Practices.Prism.ViewModel;

    /// <summary>The async notification object.</summary>
    public abstract class AsyncNotificationObject : NotificationObject
    {
        #region Constructors and Destructors

        /// <summary>Initializes a new instance of the <see cref="AsyncNotificationObject"/> class.</summary>
        protected AsyncNotificationObject()
        {
            Dispatcher = Application.Current.Dispatcher;
        }

        #endregion

        #region Properties

        /// <summary>Gets or sets Dispatcher.</summary>
        protected Dispatcher Dispatcher { get; set; }

        #endregion

        #region Methods

        /// <summary>The raise property changed.</summary>
        /// <param name="propertyName">The property name.</param>
        protected override void RaisePropertyChanged(string propertyName)
        {
            if (Dispatcher.CheckAccess()) base.RaisePropertyChanged(propertyName);
            else Dispatcher.BeginInvoke(() => base.RaisePropertyChanged(propertyName));
        }

        #endregion
    }
}

【讨论】:

    【解决方案4】:
    1. 总是绑定表的DataView,而不是直接绑定DataTable。表格的视图版本,DataView 有ListChanged,DataRowView 有PropertyChanged
    2. WPF 支持更新到行级别。如果您更改行值,它肯定会立即传播。
    3. PropertyChanged 不是线程安全的。您不能导致任何更改在不同的线程上触发 PropertyChanged。它必须在调度程序上完成,因此必须通过调度程序进行更改。 例如,您应该使用 Dispatcher.Invoke(new Action(model =&gt; model.Data = newData), Model) 或类似名称,而不是 Model.Data = newData

    【讨论】:

    • 感谢阿斯蒂的反馈。我在article 中似乎已经看到 ClrBindingWorker 已经编组线程之间的操作......这有意义吗?我正在通过 Invoke 对其进行编组,这很有帮助(现在,我的内存泄漏似乎是由我的最新更改引起的 - 引入了调用 - 但我尚未确认)。
    • 不过,仅使用调度程序不太可能发生泄漏。
    • 阿斯蒂,我的想法完全正确。我正在重新测试应用程序而不进行任何更改,以确保泄漏不是由对调度程序的调用引入的,并且已经存在于应用程序中。但这很难测试,因为在泄漏出现之前需要大约 18 小时的连续运行(大约 8000 次连续调用)。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多