I have used delegates in the past to update the current UI (for example a textbox) with a value that is being processed in my thread. I was under the assumption that async and await took care of this for you.
When I run the code below -when the button is pressed start a new task that will count up to 100 every 100ms. The count should be displayed on the UI, but I get a targetInvocationException instead:
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
textBox1.Text = "Press button";
}
private async void button1_Click(object sender, EventArgs e)
{
await Task1();
}
private Task Task1()
{
return Task.Factory.StartNew(() => CountMethod());
}
private void CountMethod()
{
for (int i = 0; i < 100; i++)
{
Task.Delay(100).Wait();
//Console.WriteLine(i.ToString());
textBox1.Text = i.ToString();
}
}
}
The problem is that you are using a thread-pool thread to access the UI. You are doing this by using the Task.Factory.StartNew method.
Here is how you can fix it:
private async Task Task1()
{
for (int i = 0; i < 100; i++)
{
await Task.Delay(100);
textBox1.Text = i.ToString();
}
}
await will capture the current SynchronizationContext so that after the asynchronous wait is complete, the code that follows executes on the UI thread. This is true because await was called by the UI thread (in the case of my example).
In your current code, await is called on a thread-pool thread. In this case the current SynchronizationContext is null and therefore the code that follows the asynchronous wait will be executed also on a thread-pool thread.
Related
I have this method that i need to pause for some period of time.
This is what I tried:
private async void btnStart_Click(object sender, RoutedEventArgs e)
{
await Task.Run(() =>
{
this.Dispatcher.Invoke((Action)(() =>
{
NewGame();
// Game Loop
for (int i = 0; i < 35; i++)
{
int index = _random.Next(0, _numbersInGame.Count);
int number = _numbersInGame.ToArray()[index];
lblCurrentNumber.Content = number.ToString();
Task.Delay(1000).Wait(); // I want to add pause here
(this.FindName($"lblNum{i + 1}") as Label).Content = number.ToString();
lblCurrentNumber.Content = "";
_numbersInGame.RemoveAt(index);
}
}));
});
}
Any help is welcome.
Thanks!
You don't need to offload this to a task (with Task.Run)
Since you aren't offloading, you don't need to marshal back to the UI thread with Invoke.
Given the above, you can idiomatically await a Task.Delay without blocking the UI
Example
private async void btnStart_Click(object sender, RoutedEventArgs e)
{
for (int i = 0; i < 35; i++)
{
NewGame();
int index = _random.Next(0, _numbersInGame.Count);
int number = _numbersInGame.ToArray()[index];
lblCurrentNumber.Content = number.ToString();
await Task.Delay(100);
(this.FindName($"lblNum{i + 1}") as Label).Content = number.ToString();
lblCurrentNumber.Content = "";
_numbersInGame.RemoveAt(index);
}
}
Using the async and await pattern like this allows you to safely update the UI as the continuation will be posted back to the UI context. It also allows the Message Pump / Dispatcher to continue unabated until the awaited delay has completed.
Lastly, you might want to protect this method from double clicks with a flag or similar (since the ui is free to reenter)
I have looked around the internet and found a nice solution which I am incorporating into my code below however it doesn't quite do exactly what I want, it works when just calling an update but I want to run a method in another class then let that method call the method that will report back to the UI and just pass some information so this mock up is just changing the button content before the operation is ran.
Using a Dispatcher I can get a UI control to update however I don't just wish to do that I want to perform some functions then have the UI Update.
So there maybe some theory I am not getting, I know the Invoke is a synchronous operation and breaking through the code it does run but the UI doesn't update.
MainWindow
Has a single button with content "CLICK ME"
Code Behind
public partial class MainWindow : Window
{
public static Button windowButton;
public MainWindow()
{
InitializeComponent();
windowButton = btnStart;
}
private void btnStart_Click(object sender, RoutedEventArgs e)
{
// Run a Process in another class on a different Thread
ProcessClass processClass = new ProcessClass();
Task processTask = new Task(() =>
{
processClass.DoSomething();
});
processTask.Start();
}
}
}
ProcessClass
class ProcessClass:MainWindow
{
public static void UpdateUI()
{
App.Current.Dispatcher.Invoke(delegate
{
windowButton.Content = "CHANGED CONTENT";
});
}
public void DoSomething()
{
UpdateUI();
int counter = 0;
for(int i = 1; i < 100; i++)
{
counter += i;
Thread.Sleep(100);
}
MessageBox.Show($"Task Completed, answer is {counter}");
}
}
Assuming that ProcessClass is your own code that you can update, change the signiture of DoDomething() to
public async Task DoSomething(IProgress<string> progress)
{
progress.Report("Begin DoSomething()");
var counter = 0;
for(var i = 1; i < 100; i++)
{
counter += i;
await Task.Delay(100).ConfigureAwait(false);
progress.Report($"DoSomething() - i = {i}");
}
progress.Report($"DoSomething() Completed, answer is {counter}");
}
Now your button click handler can be written
private async void btnStart_Click(object sender, RoutedEventArgs e)
{
// usually you would update some other control such as a TextBlock
// for the feedback, rather than the button content
var progress = new Progress<string>(s => btnStart.Content = s);
ProcessClass processClass = new ProcessClass();
await processClass.DoSomething(progress).ConfigureAwait(false);
}
Controlling the progress bar with the backgroundworker made my project difficult after a certain place. In this case, I decided to move on async structure, I built the architecture on the async structure. But this time I did not know how to control the progress bar under the async structure.
private async void button3_Click(object sender, EventArgs e)
{
progressBar1.Value = 1;
int value = 1;
await ProgressBarControl(value);
await Convert();
}
public Task ProgressBarControl(int e)
{
return Task.Run(() =>
{
var progress = new Progress<int>(percent =>
{
progressBar1.Value = percent;
});
});
}
But it is not working. I used backgroundworker. I was asking this question. But you guys suggested to me backgroundworker. But backgroundworker, after a while have a error and system is not answered to me?
The question is unlcear. BackgroundWorker is osbsolete since 2012 anyway.
If you want to report progress from any background thread, not just threads created with Task.Run, create a Progress<T> in the UI thread and pass it only as an IProgress<T> to the background thread or task, eg :
private async void button3_Click(object sender, EventArgs e)
{
var progress = new Progress<int>(percent =>
{
progressBar1.Value = percent;
});
progressBar1.Value = 1;
int value = 100;
await DoSomeWork(value,progress);
}
public Task DoSomeWork(int iterations,IProgress<int> progress)
{
for(int i=0;i<iterations;i++)
{
await Task.Run(()=>{
DoSomethingReallySlow(i);
progress.Report(i*100/iterations));
});
}
}
Check Async in 4.5: Enabling Progress and Cancellation in Async APIs for an example of both progress reporting and cancellation
Very simple example:
<StackLayout>
<Button Text="{Binding LoginButtonText}" Clicked="Button_Clicked"></Button>
</StackLayout>
Code behind:
public partial class ItemsPage : ContentPage
{
private ViewModels.ItemsViewModel _viewModel;
public ItemsPage()
{
_viewModel = new ItemsViewModel();
BindingContext = _viewModel;
InitializeComponent();
}
private void Button_Clicked(object sender, EventArgs e)
{
this._viewModel.LoginButtonText = "Start" + DateTime.Now.ToString();
// this loop is just for testing purposes. To demonstrate
// that this loop block UI thread
for (int i = 0; i < 100; i++)
{
for (int j = 0; j < 1000; j++)
{
string s = new Random(45).NextDouble().ToString();
}
}
this._viewModel.LoginButtonText = "End " + DateTime.Now.ToString();
}
}
I'm using MVVM - INotifyPropertyChanged
public class ItemsViewModel : ObservableObject
{
private string _loginButtonText;
public string LoginButtonText
{
get { return _loginButtonText; }
set { SetProperty(ref _loginButtonText, value); }
}
}
ObservableObject implementation can be seen here: https://codeshare.io/G87N74
When I click on the button after a few seconds (4 or 5) the button text gets value 'End 03/08/2017 08:55:33' (will it depends on the current timestamp of course). The button text Start + DateTime does not appear.
It works if I write this:
private void Button_Clicked(object sender, EventArgs e)
{
this._viewModel.LoginButtonText= "Start" + DateTime.Now.ToString();
// I assume here execution switches threads as it sees Task as a new thread. While waiting for task to finish
// it finished mvvm INotifyPropertyChanged change.Invoke call and updates the button text
await Task.Delay(5000);
this._viewModel.LoginButtonText = "End " + DateTime.Now.ToString();
}
But it's not 100% as we don't know how threads will be scheduled for execution. Is there a simple way to update UI immediately when we hit event method?
There is a method Device.BeginInvokeOnMainThread() but it returns void, therefore it doesn't block UI.
We cannot make changes to UI immediately from background thread. All operations on UI thread from background thread will execute on next UI thread cycle. And if your application is not blocked by some intensive tasks, it will be as close to immediately as we can reach.
If you want to make your first example to work well, put that heavy operations to backgorund task:
this._viewModel.LoginButtonText = "Start" + DateTime.Now.ToString();
//With await Task.Run (creating new thread) on heavy operation this thread is not blocked
//so previous operation is scheduled to UI thread and changes will appear on screen
await Task.Run(() =>
{
for (int i = 0; i < 100; i++)
{
for (int j = 0; j < 1000; j++)
{
string s = new Random(45).NextDouble().ToString();
}
}
});
this._viewModel.LoginButtonText = "End " + DateTime.Now.ToString();
Also here is documentation from Xamarin about threads:
Application user interfaces are always single-threaded, even in multi-threaded devices – there’s only one representation of the screen and any changes to what is displayed need to be coordinated through a single ‘access point’. This prevents multiple threads from trying to update the same pixel at the same time (for example)!
I am using Task class in my app. This is NOT WPF application! The question is are there any possibilities of calling function from Task body on UI thread, like this:
var task = new Task(() => DoSmth(1));
task.Start();
public void DoSmth(int arg)
{
//smth
CallNotifFuncOnUIThread(() => Notify(1));
//smth ELSE
CallNotifFuncOnUIThread(() => Notify(2));//notify AGAIN
//smth ELSE
}
public void Notify(int arg)
{
progressBar1.Value = arg;
}
Or maybe there are other solutions of this problem? I know about BackgroundWorker class, but what about Tasks?
You can always call other methods inside your DoSth()
Dispatcher.Invoke(...);
Dispatcher.BeginInvoke(...);
You can also user Task.ContinueWith(...) to do sth after the task is finished processing ...
If you have a task you can start it on the gui thread by providing the correct scheduler:
Task.Factory.StartNew(() => DoSomethingOnGUI(), TaskScheduler.FromCurrentSynchronizationContext());
UI is usually STA see: http://msdn.microsoft.com/en-us/library/windows/desktop/ms680112(v=vs.85).aspx
so in order to do something from none UI thread in the UI you need to inject somehow the msg into the thread
see for example htis winform example:
http://weblogs.asp.net/justin_rogers/articles/126345.aspx
watever UI you are using you will need a similar system.
With windows Form and progressBar1 component on ityou can use TPL IProgress interface for Task.
private void Form1_Load(object sender, EventArgs e)
{
Progress<int> progress = new Progress<int>();
var task = Alg(progress);
progress.ProgressChanged += (s, i) => { UpdateProgress(i); };
task.Start();
}
public void Notify(int arg)
{
progressBar1.Value = arg;
}
public static Task Alg(IProgress<int> progress)
{
Task t = new Task
(
() =>
{
for (int i = 0; i < 100; i++)
{
Thread.Sleep(100);
((IProgress<int>)progress).Report(i);
}
}
);
return t;
}