【问题标题】:WPF - Update bound property from non-UI thread [duplicate]WPF - 从非 UI 线程更新绑定属性 [重复]
【发布时间】:2020-03-13 19:48:29
【问题描述】:

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

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"/>

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

  • 有人说 Dispatcher 对于集合是必需的,但对于单个变量则不需要:那么为什么这个其他示例仍然没有引发异常?
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]"/>
  • 有人说这是以前的 .NET 版本所要求的:那么现在我们可以删除那些 Dispatcher 调用(我使用的是 .NET Framework 4.7)吗?
  • 有人说使用 Dispatcher 是一种很好的做法,即使您的代码没有引发异常:这似乎是一种信仰行为...

【问题讨论】:

  • 是的,您需要使用 Dispatcher 来更新 WPF 控件。如有疑问,请阅读documentation
  • @Zer0 所以这只是一个随机且特定的场景,上面提供的代码没有抛出异常(在重复的测试运行中)?难以置信.....
  • 这并不罕见。自 WinForms 以来,这个线程问题一直存在。我建议您了解为什么需要用[STAThread] 标记 WPF GUI 线程。仅仅因为控件在从后台线程更新时不会引发异常,not 就使其成为一个好主意。一直不安全。

标签: c# wpf multithreading data-binding dispatcher


【解决方案1】:

您的第一个示例不是从禁止线程修改任何与DispatcherObject 相关的 UI 线程。绑定引擎自动将INotifyPropertyChanged.PropertChanged 事件或其注册的事件处理程序的调用编组到UI 线程。这意味着 UI 的 Binding.Target,即 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; 
  });

每个派生自 DispatcherObjecte.g. 的对象通过将TextBox 映射到线程的Dispatcher,将TextBox 与创建它的线程相关联(线程亲和性)。只有这个线程被允许修改DispatcherObject。其他线程必须使用关联的DispatcherDispatcherObject 所有者线程的SynchronizationContext

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

【讨论】:

  • 重点是,这一切都与DispatcherObject有关。如果实例不是DispatcherObject,则可以从任何线程对其进行修改。要获得跨线程异常,DispatcherObject 必须绑定到它。如果将ListBox 绑定到它,则会引发异常,因为ListBox 派生自DispatcherObject
  • 现在,当INotifyCollectionChanged.CollectionChanged 从与创建DispatcherObject 的线程不同的线程引发时,将引发异常,因为事件处理程序是在引发事件的线程上执行的。例如的事件处理程序ItemsControl 将更新(修改)控件 (DispatcherObject) 以更新自身。如果这发生在错误的线程上,则抛出跨线程异常。
  • 现在是实现细节。 ItemsControl.ItemsSource 属性的类型为 IEnumerable,以提供最大的灵活性。这就是为什么你可以绑定例如ListDataTable 给它。现在,当您为ItemsControl.ItemsSource 属性分配值时,ItemsControl(或更准确地说是ItemContainerGenerator)检查分配的IEnumerable 是否实现INotifyCollectionChanged。如果是这样,则控件订阅CollectionChanged 事件以获取有关集合更改的通知,以便它可以更新自身以呈现更改。
  • 但是TextBlock 不是这样实现的。 TextBlock 并非旨在显示项目集合。它期望将string 值分配给Text 属性。所以给这个属性赋值不会导致检查它是否是INotifyCollectionChanged,因此TextBlock永远不会订阅CollectionChanged事件,这就是为什么没有事件处理程序可以修改TextBlock并且可能在错误的线程上执行。实际的属性 value 本身从未改变,因为它仍然是 e.g. 的同一个实例。 MyBoundPropertyCollection.
  • TextBlock 只会尝试通过调用object.ToString 将非string 转换为string。这就是为什么您将集合类型视为Text 属性的值。如果您想了解 .NET 类型是如何实现的,请查看 Microsoft .NET Reference Source
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-09-17
  • 1970-01-01
  • 1970-01-01
  • 2021-12-13
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多