多线程和WPF的数据绑定

4

情况

我的应用程序出现了不一致的行为:在大约20次执行中,一个绑定到DataTable的WPFToolkit的DataGrid不会呈现所有行,缺少预期的4行中的1至3行。

内部工作原理

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

因此,换句话说,我清除表格,通知WPF,插入数据,通知WPF并退出。

除了DataTable之外,还有许多属性(主要是字符串和整数)被更新并因此得到通知。我们没有在任何其他情况下观察到这种行为,只有在DataTable中出现。

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


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

4

DataTable 是 WPF 之前的技术,因此不支持INotifyCollectionChanged。这个接口是WPF用来监控集合变化的方式。你有两个选择:

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

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


David,我在加载完行后才通知的。由于应用程序只从用户刺激中检索数据,因此我不需要在添加过程中通知屏幕。无论如何,你说WPF仅在认为数据有所不同时才更新屏幕。它是如何做到的? - Bruno Brant
@BrunoBrant,WPF通过保留对已绑定数据的引用并将其与新引用进行比较来知道数据是否不同。您可以在此处阅读更多信息:(http://www.lhotka.net/weblog/CommentView,guid,06f305de-ec32-4e20-b042-171b58f305ae.aspx)和(http://kentb.blogspot.com/2007/03/beware-datacontext-and-equals.html)。我忘记提到另一个选项。在触发第一个PropertyChanged通知之前将实际属性设置为null,然后在触发最后一个通知之前将其设置回您的数据表。 - David
但我并没有改变DataContext,我只是更新了对象的一个属性。Equal()方法仍然用于确定更改吗?我的DataContext不是DataTable,而是一个具有DataTable属性的对象。 - Bruno Brant
@DataContext 属性也是一个属性。这个检查适用于所有绑定,因为它使用反射,并且是一种耗费昂贵的操作,特别是在数据集合的情况下。 - David

2

根据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
    }
}

2
您是否将新数据加载到已绑定到DataGrid的相同DataTable实例中?
如果是这样,那么(a)每次您从后台代码更改DataTable时,它都会从错误的线程触发通知,这是不允许的;(b)当您最后触发PropertyChanged时,DataGrid可能聪明到注意到引用实际上并没有更改,因此它不需要执行任何操作。 (我不知道DataGrid是否尝试变得聪明,但这并不是不合理的 - 特别是考虑到WPF如何在集合之上构建视图 - 这可能有助于解释您看到的症状。)
尝试每次需要刷新时创建一个新的DataTable实例,然后在后台线程中完成从该实例填充,然后将新的(完全填充)引用分配到您的通知属性中,并触发PropertyChanged(当然,确保从UI线程执行分配+PropertyChanged)。

Joe,为什么我需要从UI线程执行这个操作?据我所知,PropertyChanged已经使用了调度程序来执行此操作... - Bruno Brant
@BrunoBrant,似乎PropertyChanged通知确实会跨线程传递,但我并没有看到保证这一点的文档,而且我也见过MVVM框架自己进行跨线程通知。因此,我不确定要信任多少。如果您看到奇怪的行为,我倾向于首先消除所有未知因素。 - Joe White
谢谢你的建议。我正在进行编组,它似乎能够解决问题...但到目前为止,我只使用了Invoke进行测试,而没有使用BeginInvoke,这让我怀疑,也许通过使用Invoke,我可以最小化错误发生的概率... - Bruno Brant

1
  1. 不要直接绑定 DataTable,而是始终绑定表的 DataView。作为表的视图版本,DataView 具有 ListChanged 事件,DataRowView 具有 PropertyChanged 事件。
  2. WPF 支持对行级别进行更新。如果更改了某一行的值,它将立即传播。
  3. PropertyChanged 不是线程安全的。您不能在不同的线程上触发 PropertyChanged 的任何更改。必须通过调度程序来完成更改。例如,不要使用 Model.Data = newData,而应该使用 Dispatcher.Invoke(new Action(model => model.Data = newData), Model) 或类似的方式。

感谢您的反馈,Asti。我在这篇文章中看到 ClrBindingWorker 已经在线程之间进行了操作调度... 这样做是否有意义?我通过 Invoke 进行操作调度,这很有帮助(现在,我有一个内存泄漏的问题,似乎是由我最近的更改引起的——即引入调用——但我还需要确认它)。 - Bruno Brant
使用调度程序很少会导致泄漏。 - Asti
Asti,我的想法和你一样。我正在重新测试该应用程序,以确保内存泄漏不是由对调度程序的调用引入的,而是已经存在于应用程序中。但这种测试很难,因为需要连续运行大约18小时(大约8000次连续调用)才能出现内存泄漏问题。 - Bruno Brant

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接