winforms: updating progress with a parallel.foreach - c#

I haven't seen any posts pertaining to my issue, so I apologize if I post a question already asked.
I have a windows form program, c#, that checks stocks and does analysis. The main form launches another form, via a new thread and ShowDialog. While it's loading, it's running a parallel.foreach. In that parallel.foreach, I'd like to show progress on the main form.
I've run into cross-threading issues, and added invoke, although it doesn't appear to be thread-safe as it seems to be deadlocking toward the end of the parallel.foreach. I've tried delegates, events, no luck. Help me Obi-Wans, you're my only hope!
Stripped down version:
Main form
private void button1_Click(object sender, EventArgs e)
{
YearLows yearLows = new YearLows();
Thread yearLowsThread = new Thread(() => StartYearLows(yearLows));
yearLowsThread.Start();
btnGetYearLows.Enabled = false;
}
private void StartYearLows(YearLows yearLows)
{
yearLows.ShowDialog();
}
public void UpdateProgress(string text)
{
lblProgress.Text = text;
}
2nd form dialog
public partial class YearLows : Form
{
private void YearLows_Load(object sender, EventArgs e)
{
// work
Parallel.ForEach(responseStocks, new ParallelOptions { MaxDegreeOfParallelism = MaxThreads }, x =>
{
// more work
Interlocked.Increment(stocksProcessed);
UpdateProgress($"{stocksProcessed} / {stocksTotal} Researched");
});
}
private void UpdateProgress(string text)
{
Invoke(new Action(() => frmMain.UpdateProgress(text)));
}
}
Update 1:
If I move the progress update label to the child form, it appears I am getting all the progress updates. I had to move from the Load event to the Shown event so that the form renders, so users can see the progress updates. I had to follow SLaks advice though and run Task.Run(() => Parallel.ForEach. This will work for me. Would still like to figure out why it still locks up toward the end if I wanted the progress updates on the main form. (I've always read async void was bad, but I guess no way around this in these defined method signatures in winforms)
public partial class YearLows : Form
{
private async void YearLows_Shown(object sender, EventArgs e)
{
await AnalyzeStocks();
}
private async Task AnalyzeStocks(object sender, EventArgs e)
{
// work
await Task.Run(() => Parallel.ForEach(responseStocks, new ParallelOptions { MaxDegreeOfParallelism = MaxThreads }, x =>
{
// more work
Interlocked.Increment(stocksProcessed);
UpdateProgress($"{stocksProcessed} / {stocksTotal} Researched");
}));
}
private void UpdateProgress(string text)
{
Invoke(new Action(() => lblProgress.UpdateProgress(text)));
}
}

Parallel.ForEach is a blocking call; it runs delegates on the calling thread too. Therefore, the UI cannot update until it finishes.
Instead, you should use await with Task.WhenAll (if you're doing async work) or Task.Run(() => Parallel.ForEach(...)) (if it's CPU-bound) so that you leave the UI thread idle and able to update.

you can use Async Await function for this puprose... this link can be more useful to you...
PictureBox animation freezing while Task running

As per SLaks answer, an example of using Task.Run, with UI update
var tasks = new List<Task>();
foreach (var result in results)
{
tasks.Add(Task.Run(async () => {
// DO WORK
Dispatcher.Invoke(() =>
{
// UPDATE THE UI, I.E. ProgressBar.Value++;
});
}));
}
await Task.WhenAll(tasks);

Related

C# - How to send update value back to form class from a class where the work is done in a different thread

I'm working on a Splash/Loading screen for an app and I'd like to have a progress bar as well as a label on it which will notify the user of current process, e.g. connecting to the database, loading user settings, retrieving x data, retrieving y data.
I only did a tiny bit of BackGroundWorker stuff in VB before and nothing in C# so a bit confused where to start as it looks quite different in C#.
I'd like to keep the form class simple as to only calling specific methods from different classes on a different thread.
I mainly need to know how to update the GUI from the class that does the work as I think I could work out the threading itself from the below code:
using System;
using System.Threading;
public class ThreadWork
{
public static void DoWork()
{
for(int i = 0; i<3;i++) {
Console.WriteLine("Working thread...");
Thread.Sleep(100);
}
}
}
class ThreadTest
{
public static void Main()
{
Thread thread1 = new Thread(ThreadWork.DoWork);
thread1.Start();
for (int i = 0; i<3; i++) {
Console.WriteLine("In main.");
Thread.Sleep(100);
}
}
}
You could use the Task.Run method to offload work to a ThreadPool thread, and a Progress<string> object to report progress from the background thread to the UI. You can also use async/await in order to write your code in a straightforward way. Here is an example:
private async void Form_Load(object sender, EventArgs e)
{
IProgress<string> progress = new Progress<string>(message =>
{
Label1.Text = message;
});
var result = await Task.Run(() =>
{
progress.Report("Connecting to database...");
ConnectToDatabase();
progress.Report("Loading user settings...");
LoadUserSettings();
progress.Report("Retrieving x data...");
return RetrieveXData();
});
// At this point the background processing has completed
Label1.Text = $"Done! ({result})";
}
If you have heterogeneous data to report at different intervals, you can use multiple Progress<T> objects (for example a Progress<string>, a Progress<int> etc). You can also use a complex type for reporting, for example Progress<(string, int)>.
Although Mitch's answer will work. You can also use TaskSchedulers to handle background work and then respond back to UI:
public static void Main()
{
TaskScheduler uiScheduler = TaskScheduler.FromCurrentSynchronizationContext();
Task.Factory.StartNew(() => DoBackgroundWork(uiScheduler));
}
private static void DoBackgroundWork(TaskScheduler uiScheduler)
{
// Do background stuff here...
Task.Factory.StartNew(() =>
{
// Respond back to UI here
Console.WriteLine("Doing work");
// Or if you have to change GUI, let's say a label value;
label1.Text = "Text changed by background Task";
}, CancellationToken.None, TaskCreationOptions.None, uiScheduler);
// Some other background work
}

How to update a Windows Form textbox value while waiting for a web response

I'm trying to update a status message while waiting for a web response to be returned. The call posts files to a server and sometimes it can take 30+ seconds.
I want to update the message (windows form textbox text) if the call is taking longer than expected. If the call has been waiting for 15 seconds, update the message to "This is taking awhile but should complete soon."
I've tried:
async fire and forget
timer using invoke
task.run
both tasks as async, awaiting the web calling Tasks
Background Worker using dowork and progress work
Nothing seems to work. Is it even possible to update the main thread while a task has a thread locked up?
I'm testing with simple calls:
private void worker_DoWork(object sender, DoWorkEventArgs e)
{
while (!worker.CancellationPending)
{
Task.Delay(1000).Wait();
this.Invoke((MethodInvoker)delegate
{
this.box.Text += '.';
this.box.Update();
});
}
}
private void MakeCall()
{
worker.RunWorkerAsync();
using (WebClient client = new WebClient())
{
//Just runs Task.Delay(10000) then returns "Complete"
var res = client.DownloadString("https://localhost:44343/api/TestDelay");
MessageBox.Show(res);
}
worker.CancelAsync();
}
private void button_Click(object sender, EventArgs e)
{
MakeCall();
}
I think I see your problem. You are downloading on the UI thread without ever getting off of it, so the background worker can never get on it either.
Try this code out:
EDIT: Using two Tasks rather than a background worker
private void MakeCall()
{
// it'd be a good idea to disable the button here
ManualResetEventSlim waiter = new ManualResetEventSlim(false);
Task.Run(async () =>
{
while(!waiter.IsSet)
{
await Task.Delay(1000);
this.Invoke((MethodInvoker)delegate
{
this.box.Text += '.';
this.box.Update();
});
}
});
Task.Run(() =>
{
using (WebClient client = new WebClient())
{
var res = client.DownloadString("https://localhost:44343/api/TestDelay");
MessageBox.Show(res);
}
waiter.Set();
// hop back on the UI thread and re-enable your button here
});
}
private void button_Click(object sender, EventArgs e)
{
MakeCall();
}
I'm providing an additional answer here because although the answer by outbred worked in my test, it didnt work in my original program because the form needed to remain open, locking the main form, run one of the defined tasks, then close automatically. That meant the async fire and forget method wasn't an option.
What I did was overloaded the forms ShowDialog method to take an async action, trigger it, show the dialog, then close the dialog on complete.
This method works perfectly, locking the parent and allowing the background worker to update the text.
internal DialogResult ShowDialog(Action action) => ShowDialog(async () => await Task.Run(action));
internal DialogResult ShowDialog(Func<Task> action)
{
action.Invoke().ContinueWith(task => this.DialogResult = DialogResult.OK);
return this.ShowDialog();
}
Then you can call it using one of the following ways:
using (Form1 form = new Form1())
form.ShowDialog(form.MakeCall);
using (Form1 form = new Form1())
form.ShowDialog(() => { form.MakeCall("HelloWorld");} );
using (Form1 form = new Form1())
form.ShowDialog(async () => { await form.MakeCallAsync("HelloWorld");} );
It will display the form as a dialog (locking the parent), run the task to completion, then close the form.

async/await running on single thread

I have a console application where in some instances a user interface needs to be presented. This user interface needs to remain responsive as it will contain a loading gif, progress bar, cancel button etc. I have the following sample code:
class Program
{
static void Main(string[] args)
{
DoWork().GetAwaiter().GetResult();
}
private static async Task DoWork()
{
TestForm form = new TestForm();
form.Show();
string s = await Task.Run(() =>
{
System.Threading.Thread.Sleep(5000);
return "Plop";
});
if (s == "Plop")
{
form.Close();
}
}
}
I would expect from the code above for the TestForm to be displayed for approximately 5 seconds before being closed due to the value of the string being "Plop", however all that happens is the Task is run and the if statement is never reached. Furthermore the UI of the TestForm does not remain responsive. What is wrong with this code?
So I've managed to hack together a dirty solution for this. It is not a clean solution so I'm still open to suggestions but for what I need it works fine
private static void DoWork()
{
TestForm form = new TestForm();
Task formTask = Task.Run(() => form.ShowDialog());
Task<string> testTask = Task.Run(() =>
{
for (int i = 1; i < 10; i++)
{
Thread.Sleep(1000);
Console.WriteLine(i.ToString());
}
Console.WriteLine("Background task finished");
return "Plop";
});
Console.WriteLine("Waiting for background task");
testTask.Wait();
if (testTask.Result == "Plop")
{
Dispatcher.CurrentDispatcher.InvokeAsync(() => form.Close());
}
Console.WriteLine("App finished");
}
This outputs 'Waiting for background task' first, followed by the number count of the Task and then outputs 'Background task finished' when the long process is complete, as well as closes the responsive UI form
Its a classic deadlock.When your code hit await ,control goes back to main thread which is a blocking wait for DoWork GetResult(); When Task.Run thread is finished controls tries to go back to main thread but its waiting for DoWork to be finished. That is the reason last If statement never executes.
But apart from deadlock ,there is also one more issue in your code which will make your UI freeze.Its the form.Show() method.If you remove everything related to async-await and only use form ,it will still freeze.The problem is Show method expects a windows message loop which will be provided if you create a Windows.Forms application but here you are launching form from console application which doesnt have a message loop. One solution would be to use form.ShowDialog which will create its own message loop. Another solution is to use System.Windows.Forms.Application.Run method which provides a win messages loop to the form created through thread pool thread. I can give you one possible solution here but its up to you how you structure your code as the root cause is identified.
static void Main(string[] args)
{
TestForm form = new TestForm();
form.Load += Form_Load;
Application.Run(form);
}
private static async void Form_Load(object sender, EventArgs e)
{
var form = sender as Form;
string s = await Task.Run(() =>
{
System.Threading.Thread.Sleep(5000);
return "Plop";
});
if (s == "Plop")
{
form?.Close();
}
}
Ok I did mark my first answer to be deleted, since what I put there works for WPF and not for you require, BUT in this one is doing what you asked, I did try it and opens the WinForm then closes after 5 seconds, here is the code:
static void Main(string[] args)
{
MethodToRun();
}
private static async void MethodToRun()
{
var windowToOpen = new TestForm();
var stringValue = String.Empty;
Task.Run(new Action(() =>
{
Dispatcher.CurrentDispatcher.InvokeAsync(() =>
{
windowToOpen.Show();
}).Wait();
System.Threading.Thread.Sleep(5000);
stringValue = "Plop";
Dispatcher.CurrentDispatcher.InvokeAsync(() =>
{
if (String.Equals(stringValue, "Plop"))
{
windowToOpen.Close();
}
}).Wait();
})).Wait();
}

C# - Cross Thread Error using Async and Await on TextBox

I am new to Async and Await and have created a simple project in order to understand how it works.
For this, I have a simple Windows Form application that has 2 elements:
Get Completed Items button
TextBox showing all Completed Items retrieved
When I click the button, it should display all completed Items in the TextBox.
This is the code I have written:
private async void btnGetCompletedItems_Click(object sender, EventArgs e)
{
QueueSystem queueSystem = QueueSystem.NewInstance(75);
Stopwatch watch = new Stopwatch();
watch.Start();
await Task.Run(() => GetCompletedItems(queueSystem));
watch.Stop();
lblTime.Text = $"{watch.ElapsedMilliseconds.ToString()} ms";
}
private void GetCompletedItems(QueueSystem queueSystem)
{
foreach (var item in queueSystem.GetCompletedItems())
{
txtItems.Text += $"{txtItems.Text}{item.ItemKey}{Environment.NewLine}";
}
}
However, I am getting an error in
txtItems.Text +=
$"{txtItems.Text}{item.ItemKey}{Environment.NewLine}";
The error says
Additional information: Cross-thread operation not valid: Control
'txtItems' accessed from a thread other than the thread it was created
on.
I checked in Debug and a new thread was created for GetCompletedItems(). When I read about Async and Await, I read that it doesn't necessarily create a new thread but it seems to have created a new one for some reason.
Is my implementation and understanding of Async and Await wrong?
Is it possible to use Async and Await in a Windows Forms application?
You cannot access UI thread on a different thread. This should help
private async void btnGetCompletedItems_Click(object sender, EventArgs e)
{
QueueSystem queueSystem = QueueSystem.NewInstance(75);
Stopwatch watch = new Stopwatch();
watch.Start();
var results = await Task.Run(() => queueSystem.GetCompletedItems());
foreach (var item in results)
{
txtItems.Text += $"{txtItems.Text}{item.ItemKey}{Environment.NewLine}";
}
watch.Stop();
lblTime.Text = $"{watch.ElapsedMilliseconds.ToString()} ms";
}
You can access the thread from another thread in a following way. It does helps to avoid the cross thread exception in your application.
private void Thread()
{
this.Invoke((System.Action)(() => {
//your thread call or definition
});
}
When I read about Async and Await, I read that it doesn't necessarily create a new
thread
This is true for regular async methods. Consider this:
private async void button1_Click(object sender, EventArgs e)
{
Trace.WriteLine(Thread.CurrentThread.ManagedThreadId);
await DoesNothing();
}
private async Task DoesNothing()
{
// outputs the same thread id as similar line as from above;
// in particlar, for WinForms this means, that at this point
// we are still at UI thread
Trace.WriteLine(Thread.CurrentThread.ManagedThreadId);
await Task.Delay(1);
}
but it seems to have created a new one for some reason
This is what Task.Run is intended for:
Queues the specified work to run on the ThreadPool
In other words, it pushes anything you pass it as a delegate to a thread pool thread. Since we are in WinForms, this means, that anonymous method () => GetCompletedItems(queueSystem) will be executed at thread pool thread, not at UI one.
Here's code sample from above with little change:
private async void button1_Click(object sender, EventArgs e)
{
Trace.WriteLine(Thread.CurrentThread.ManagedThreadId);
await Task.Run(DoesNothing);
}
private async Task DoesNothing()
{
// outputs DIFFERENT thread id;
// in particlar, for WinForms this means, that at this point
// we are not at UI thread, and we CANNOT access controls directly
Trace.WriteLine(Thread.CurrentThread.ManagedThreadId);
await Task.Delay(1);
}

ObjectDisposedException when form is being closed

I have a timer on WinForm which I start when the form loads:
private void MainForm_Load(object sender, EventArgs e)
{
Action action = () => lblTime.Text = DateTime.Now.ToLongTimeString();
Task task = new Task(() => {
while (true)
{
Invoke(action);
Task.Delay(1000);
}
});
task.Start();
}
The problem is when I start the app in Debug mode in VS and the close it. I get an ObjectDisposedException which states that my form is already disposed.
I tried to fix it the following way:
private bool _runningTimer = true;
public MainForm()
{
InitializeComponent();
// ...
FormClosing += MainForm_FormClosing;
}
private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
{
_runningTimer = false;
}
private void MainForm_Load(object sender, EventArgs e)
{
Action action = () => lblTime.Text = DateTime.Now.ToLongTimeString();
Task task = new Task(() => {
while (_runningTimer)
{
Invoke(action);
Task.Delay(1000);
}
});
task.Start();
}
But the problem still ocurrs. What Am I doing wrong here?
UPDATE: I know that there is a standart timer for WinForms that works great in multithreaded invironment. I just wanted to know how it is possible to make it work to better understand how to deal with race conditions. This kind of timer is just an example, it could be another process that needs to update GUI.
UPDATE 2: Following the Hans Passant and Inigmativity answers I came to that code:
private void MainForm_Load(object sender, EventArgs e)
{
Action action = () => { lblTime.Text = DateTime.Now.ToLongTimeString(); };
Task task = new Task(async () => {
while (!IsDisposed)
{
Invoke(action);
await Task.Delay(1000);
}
});
task.Start();
}
But anyway if I make time interval, for example 100ms, the ObjectDisposedException still throws.
This is not real life example, I just experimenting with it...
In your first example the Task has no idea your app is exiting and is at risk of invoking the action after the label is destroyed, hence the ObjectDisposedException.
Though you attempt to alert the task in the second example, it isn't really that thread-safe and you could still invoke the action after the control is disposed.
Timers
A better solution is to just use a WinForms Timer. If you place the timer on the form via the designer, it automatically registers it as a component dependency making lifetime management much easier.
With WinForm timers you don't need to worry about threads or Tasks and more importantly you won't need to worry about Invoke if you need to update the UI (as opposed to child threads or non-UI context tasks)
Tell me more
How to: Run Procedures at Set Intervals with the Windows Forms Timer Component
Ok, I tried to use task cancellation the following way:
public MainForm()
{
InitializeComponent();
cts = new CancellationTokenSource();
Load += MainForm_Load;
FormClosing += MainForm_FormClosing;
}
private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
{
cts.Cancel();
}
private void MainForm_Load(object sender, EventArgs e)
{
CancellationToken ct = cts.Token;
Action action = () => { lblTime.Text = DateTime.Now.ToLongTimeString(); };
var task = Task.Factory.StartNew(async () => {
ct.ThrowIfCancellationRequested();
while (true)
{
Invoke(action);
await Task.Delay(100);
}
}, ct);
}
Don't know whether it's right but it seems works even if the time interval set to 10 ms.

Categories

Resources