在异步任务和UI线程中引发PropertyChanged

本文关键字:PropertyChanged 线程 UI 异步 任务 | 更新日期: 2023-09-27 18:01:30

在许多博客,教程和MSDN我可以读到,从非UI线程访问UI元素是不可能的-好吧,我会得到一个未经授权的异常。为了测试它,我写了一个非常简单的例子:

// simple text to which TextBlock.Text is bound
private string sample = "Starting text";
public string Sample
{
    get { return sample; }
    set { sample = value; RaiseProperty("Sample"); }
}
private async void firstButton_Click(object sender, RoutedEventArgs e)
{
    await Job(); // asynchronous heavy job
    commands.Add("Element"); // back on UI thread so this should be ok?
}
private async Task Job()
{
    // I'm not on UI Thread ?
    await Task.Delay(2000); // some other job
    Sample = "Changed";  // not ok as not UI thread?
    commands.Add("Element from async"); // also not ok?
}

我有一个正在异步运行的Task。在Task中,我想改变我的属性(这将提高PropertyChanged)并向ObservableCollection添加元素。因为它运行async,我不应该能够做到这一点,但我没有异常,代码工作正常。这就是我的疑惑和误解:

  • 为什么没有异常?
  • 是否可以在async任务中提高PropertyChanged ?
  • 是否可以在async Task中修改ObservableCollection,或者我应该返回Task<ICollection>并在获得结果后修改ObservableCollection -清除并填充它?
  • 我什么时候在Task上的UI线程,什么时候不是?
  • firstButton_Click上面的代码是可以在await Task后管理UI元素吗?我是否总是回到UI线程?

为了测试更多,我把我的属性更改和集合修改放在另一个线程中:

System.Threading.Timer newThreadTimer = new System.Threading.Timer((x) =>
   {
       Sample = "Changed";  // not UI thread - exception
       commands.Add("Element from async"); // not UI thread - exception
   }, null, 1000, Timeout.Infinite);
在上面的代码中,我的想法是好的-就在第一行或第二行之后,我得到了一个异常。但是第一个密码是什么呢?我的Task在UI线程上运行只是一种运气吗?

我怀疑这是非常基本的事情和我的误解,但我需要一些澄清,因此这个问题

在异步任务和UI线程中引发PropertyChanged

在等待Task时,当前线程的SynchronizationContext被捕获(特别是在TaskAwaiter捕获Task的情况下)。然后,继续被封送回该SynchronizationContext以执行方法的其余部分(await关键字之后的部分)。

让我们看看你的代码示例:

private async Task Job()
{
    // I'm not on UI Thread ?
    await Task.Delay(2000); // some other job
    Sample = "Changed";  // not ok as not UI thread?
    commands.Add("Element from async"); // also not ok?
}

当您输入await Task.Delay(2000)时,编译器隐式捕获SynchronizationContext,即当前的WindowsFormsSynchronizationContext。当await返回时,continuation在相同的上下文中执行,因为您没有显式地告诉它不要执行,这是您的UI线程。

如果您将代码更改为await Task.Delay(200).ConfigureAwait(false),则延续将不会被封送回当前的SynchronizationContext,并且会运行ThreadPool线程,导致UI元素更新抛出异常。

在计时器的例子中,Elapsed事件是通过ThreadPool线程引发的,因此为什么你会得到一个异常,你正在尝试更新一个由不同线程控制的元素。

现在,让我们一个一个地复习你的问题:

为什么我没有得到异常?

如前所述,await Task.Delay(2000)在UI线程上执行Continuation,这使得更新控件成为可能。

是否可以在异步任务中引发属性?

我不确定你所说的"Raise properties"是什么意思,但如果你的意思是引发INotifyPropertyChanged事件,那么是的,可以在UI线程上下文中执行它们。

是否可以在异步任务中修改observablecollection返回任务,并在获得结果后修改Observable - Clear它和填充它?

如果你有一个async方法,你想要更新一个UI绑定元素,确保你在UI线程上封送延续。如果该方法是从UI线程调用的,并且您await它的结果,那么延续将隐式地在您的UI线程上运行。如果你想通过Task.Run将工作卸载到后台线程,并确保你的continuation在UI上运行,你可以使用TaskScheduler.FromCurrentSynchronizationContext()捕获SynchronizationContext,并显式地传递continuation

我什么时候在UI线程上的任务,什么时候不在?

Task是对将来要做的工作的承诺。当您在UI线程上下文中对TaskAwaitable执行await时,那么您仍然在UI线程上运行。如果:

你不在UI线程中
  1. 你的异步方法当前正在一个不同于UI线程的线程中执行(要么是ThreadPool线程,要么是新的Thread线程)
  2. 您可以使用Task.Run将工作卸载到后台ThreadPool线程。

在上面的代码在firstButton_Click是可以管理UI元素在等待任务之后?我是否总是回到UI线程?

你将回到UI线程,只要你不明确地告诉你的代码不要返回到当前上下文使用ConfigureAwait(false)

PropertyChanged事件由WPF自动分配给UI线程,因此您可以修改从任何线程引发该事件的属性。在。net 4.5之前,ObservableCollection.CollectionChanged事件不是这种情况,因此,从UI线程以外的线程中添加或删除元素会导致异常。

从。net 4.5开始,CollectionChanged也会自动分配给UI线程,所以你不需要担心这个。尽管如此,您仍然需要从UI线程访问UI元素(例如按钮)。