首页 > 解决方案 > WPF - 从非 UI 线程更新绑定属性

问题描述

我不明白是否必须使用 Dispatcher 通知 UI 线程绑定的属性已由非 UI 线程更新。
从一个基本示例开始,为什么下面的代码没有抛出著名的(我记得在过去的编码经验中为此苦苦挣扎)调用线程无法访问该对象,因为不同的线程拥有它异常?

private int _myBoundProperty;
public int MyBoundProperty { get { return _myBoundProperty; } set { _myBoundProperty = value; OnPropertyChanged(); } }
private void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public void Test()
{
    new System.Threading.Thread(() =>
    {
        try
        {
            MyBoundProperty += DateTime.Now.Second;
        }
        catch(Exception ex)
        {
        }
    }).Start();
}

<TextBlock Text="{Binding MyBoundProperty"/>

我看到了一些关于这个主题的帖子,但在我看来它们很混乱:

private ObservableCollection<int> _myBoundPropertyCollection;
public ObservableCollection<int> MyBoundPropertyCollection { get { return _myBoundPropertyCollection; } set { _myBoundPropertyCollection = value; OnPropertyChanged(); } }
private void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public void TestCollection()
{
    new System.Threading.Thread(() =>
    {
        try
        {
            //MyBoundPropertyCollection = new ObservableCollection() { 0 }; //even so not throwing
            MyBoundPropertyCollection[0] += DateTime.Now.Second;
        }
        catch(Exception ex)
        {
        }
    }).Start();
}

<TextBlock Text="{Binding MyBoundPropertyCollection[0]"/>

标签: c#wpfmultithreadingdata-bindingdispatcher

解决方案


您的第一个示例没有修改与DispatcherObject禁止线程相关的任何 UI 线程。绑定引擎自动将INotifyPropertyChanged.PropertChanged事件或其注册的事件处理程序的调用编组到 UI 线程。这意味着 UI 的Binding.Target,即 a DispatcherObject,总是在 UI 线程上正确更新。

// Safe, because PropertyChanged will always be raised on the UI thread
MyBoundProperty = DateTime.Now; 

这不适用于INotifyCollectionChanged.CollectionChanged. 从后台线程更改的集合必须手动编组对集合的修改,方法是调用为线程注册的 Dispatcher,或者捕获SynchronizationContext.Current拥有或调用线程的线程,以便能够将关键操作发布回正确的上下文(线)。

// Assuming that this is the proper thread of the DispatcherObject, 
// that binds to MyBoundPropertyCollection
SynchronizationContext capturedSynchronizationContext = SynchronizationContext.Current;

Task.Run(() =>
  {
    // Will throw a cross-thread exception
    MyBoundPropertyCollection.Add(item); 

    // Safe
    Dispatcher.Invoke(() => MyBoundPropertyCollection.Add(item)); 

    // Safe
    capturedSynchronizationContext.Post(state => MyBoundPropertyCollection.Add(item), null); 
  });

由于您的第二个示例与第一个示例相同,因此不会出于相同的原因抛出。您不是在修改集合,例如添加/插入/移动/删除,而是在此集合中包含一个项目:

Task.Run(() => MyBoundPropertyCollection[0] = DateTime.Now);

等于

Task.Run(() =>
  {
    var myBoundProperty = MyBoundPropertyCollection[0];

    // Safe, because PropertyChanged will always be raised on the UI thread
    myBoundProperty = DateTime.Now; 
  });

DispatcherObject从eg派生的每个对象都TextBox通过将其映射到线程的Dispatcher. 仅允许此线程修改DispatcherObject. 其他线程必须使用关联线程DispatcherSynchronizationContext所有者DispatcherObject线程。

如果您想将 aDispatcherObject从一个线程传递到另一个线程,例如,将 a 传递Brush给 UI 线程,那么这只有在DispatcherObject派生自时才有可能Freezable。您会调用Freezable.Freeze,这会提升线程亲和性,即Dispatcher亲和性,并允许将实例传递给其他线程。


推荐阅读