Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Raising PropertyChanged in asynchronous Task and UI Thread

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:

  • why don't I get an exception?
  • is it ok to raise PropertyChanged in async Task?
  • is it ok to modify ObservableCollection in async Task, or should I return Task<ICollection> and after obtaining the result modify the ObservableCollection- Clear it and Fill it?
  • when am I in Task on UI thread and when not?
  • 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?

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 Taskwas run on UI thread?

I suspect that this is very basic thing and my misunderstanding, but I need some clarification and thus this question.

like image 425
Romasz Avatar asked May 02 '14 07:05

Romasz


2 Answers

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:

  1. Your async method is currently executing from a thread different then the UI thread (either a ThreadPool thread or a new Thread)
  2. You offload work to a background ThreadPool thread using 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)

like image 168
Yuval Itzchakov Avatar answered Oct 23 '22 07:10

Yuval Itzchakov


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.

like image 35
jnovo Avatar answered Oct 23 '22 07:10

jnovo