At many blogs, tutorials and MSDN I can read that accessing UI elements from non-UI threads is impossible - ok, I'll get an unauthorized exception. To test it I've written a very simple example:
// 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?
}
I've a Task
which is being run asynchronously. In that Task
I want to change my property (which will raise PropertyChanged
) and add element to ObservableCollection
. As it is run async
, I shouldn't be able to do that, but I get no exception and the code is working fine. Thus my doubts and misunderstanding:
PropertyChanged
in async
Task?ObservableCollection
in async Task
, or should I return Task<ICollection>
and after obtaining the result modify the ObservableCollection
- Clear it and Fill it?Task
on UI thread and when not? firstButton_Click
is it ok to manage UI elements after await
ing the Task
? Am I always back on UI thread?To test it more I've put my property change and collection modification in other thread:
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);
In above code my thinking is ok - just after the first or second line I get an exception. But what with the first code? Is it only a luck that my Task
was run on UI thread?
I suspect that this is very basic thing and my misunderstanding, but I need some clarification and thus this question.
When awaiting on a Task
, the SynchronizationContext
of the current thread is captured (specifically in the case of Task
by the TaskAwaiter
). The continutation is then marshaled back to that SynchronizationContext
to execute the rest of the method (the part after the await
keyword).
Lets look at your code example:
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?
}
When you await Task.Delay(2000)
, the compiler implicitly captures the SynchronizationContext
, which is currently your WindowsFormsSynchronizationContext
. When the await returns, the continuation is executed in the same context, since you didn't explicitly tell it not to, which is your UI thread.
If you changed your code to await Task.Delay(200).ConfigureAwait(false)
, the continuation would not be marshalled back to your current SynchronizationContext
, and would run a ThreadPool
thread, causing your UI element update to throw an exception.
In your timer example, the Elapsed
event is raised via a ThreadPool
thread, hence why you get an exception that you are trying to update an element which is controlled by a different thread.
Now, let's go over your questions one by one:
why don't I get exception?
As said, the await Task.Delay(2000)
executed the Continuation on the UI thread, which made it possible to update your controls.
is it ok to Raise properties in async Task?
I am not sure what you mean by "Raise properties", but if you mean raise a INotifyPropertyChanged
event, then yes, it is ok to execute them not in a UI thread context.
is it ok to modify ObservableCollecition in async Task, or should I return Task and after obtaining the result modify Observable - Clear it and Fill it?
If you have an async
method and you want to update a UI bound element, make sure you marshal the continuation on the UI thread. If the method is called from the UI thread and you await
its result, then the continuation will implicitly be ran on your UI thread. In case you want to offload work to a background thread via Task.Run
and make sure your continuation is ran on the UI, you can capture your SynchronizationContext
using TaskScheduler.FromCurrentSynchronizationContext()
and explicitly pass it the continuation
when am I in Task on UI thread and when not?
A Task
is a promise of work that will be done in the future. when you await
on a TaskAwaitable
from the UI thread context, then you are still running on the UI thread. You are not in the UI thread if:
ThreadPool
thread or a new Thread
)Task.Run
.in the code above in firstButton_Click is it ok to manage UI elements after awaiting the Task? Am I always back on UI thread?
You will be back to the UI thread as long as you don't explicitly tell your code not to return to its current context using ConfigureAwait(false)
The PropertyChanged
event is automatically dispatched to the UI thread by WPF so you can modify properties that raise it from any thread. Before .NET 4.5 this was not the case for ObservableCollection.CollectionChanged
event and thus, adding or removing elements from a thread other than the UI one, would cause an exception.
Starting in .NET 4.5 CollectionChanged
is also automatically dispatched to the UI thread so you don't need to worry about that. This being said, you'll still need to access UI elements (such as a button for instance) from the UI thread.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With