在异步任务和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线程上运行只是一种运气吗?我怀疑这是非常基本的事情和我的误解,但我需要一些澄清,因此这个问题
在等待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线程的线程中执行(要么是
ThreadPool
线程,要么是新的Thread
线程) - 您可以使用
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元素(例如按钮)。