Maintain a responsive user interface with backgroundWorker and heavy animation - c#

I have a C# application (winforms and wpf, but for this question I'm focusing on the winforms one) where a backgroundWorker is used to work on a data set, and the call to ProgressChanged which then calls the form Refresh method to force a repaint. This then paints a bunch of ellipses based on the current frame of the data set.
A given frame may involve drawing anywhere between zero and several hundred ellipses.
In addition, I have a slider control that allows the user to adjust the playback rate (basically the thread.sleep value within the loop.)
When the user sets the sleep value too low, sometimes the repainting methods get queued up, and the UI becomes unresponsive. (This depends on the number of ellipses in the frame, and the speed of the computer. And the delay is 100% with the repainting on the UI, not with any other processing, which is basically just incrementing a counter and setting a label text.)
I would like to be able to detect the queuing up and automatically adjust the speed slider to accommodate a larger data set and/or slower computer. How can I tell if the UI thread is backed up with multiple calls to Map_Paint?
Current code (paraphrased):
public Map()
{
InitializeComponent();
_worker = new BackgroundWorker();
_worker.DoWork += _worker_DoWork;
_worker.ProgressChanged += _worker_ProgressChanged;
_worker.WorkerReportsProgress = true;
}
private void _worker_DoWork(object sender, DoWorkEventArgs e)
{
_frameCount = _frames.FrameCount();
// For this specific example, _frameCount may be around 30000-40000
for (var i = 0; i < _frameCount; i++)
{
var f = _frames.Frame(i + 1);
_worker.ReportProgress(i, f);
Thread.Sleep(_tickCount);
_suspend.WaitOne(); // Used to Pause the playback
}
}
void _worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
// set some variables according to state and progresspercentage snipped here
// ...
// I would like to detect at this point whether the screen repainting is backed up
// and if so, adjust the value of _tickCount to slow down the program.
this.Refresh();
}
private void Map_Paint(object sender, PaintEventArgs e)
{
// Lots of ellipsis drawing stuff here
// Maybe 0-1000 ellipses drawn per cycle.
}
private void tbSpeed_Scroll(object sender, EventArgs e)
{
// This is the Scroll event for the slider.
// Value range is 10-300
// The slider becomes unresponsive when the UI thread backs up.
// I'd like to detect the back up and override the value of _tickCount
_tickCount = tbSpeed.Value;
}

private static object _lock = new object();
private static int _queuedCount = 0;
public Map()
{
InitializeComponent();
_worker = new BackgroundWorker();
_worker.DoWork += _worker_DoWork;
_worker.ProgressChanged += _worker_ProgressChanged;
_worker.WorkerReportsProgress = true;
}
private void _worker_DoWork(object sender, DoWorkEventArgs e)
{
_frameCount = _frames.FrameCount();
// For this specific example, _frameCount may be around 30000-40000
for (var i = 0; i < _frameCount; i++)
{
var f = _frames.Frame(i + 1);
lock(_lock)
{
_queuedCount++;
}
_worker.ReportProgress(i, f);
Thread.Sleep(_tickCount);
_suspend.WaitOne(); // Used to Pause the playback
}
}
void _worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
if (_queuedCount > 1)
//now queue is building up
this.Refresh();
lock(_lock)
{
_queuedCount--;
}
}

Related

Label not visible on Panel when calling visible = true

I already tried google to find an answer to my problem but haven't found a solution.
I working with C# and WinForms. I created a panel and added a label to it. This panel is set to myPanel.Visible = falseat first. I want to set it myPanel.Visible = true when I click a button. The button is calling a function. During the function call I want to show a progessbar in the panel, so I set this myPanel.Visible = trueand at the end of the function I set it back to myPanel.Visible = false.
The problem is, that the label isn't visible.
When I don't set myPanel.Visible = false at the end of the function, the label is visible, but only at the end of the function.
I also tried to programmatically add the label in the function called, still not working. The second idea I tried was to use this.PerformLayout(); during the call of the function.
It seems like that the application is drawing the label only at the end of the function call, but I need it to be drawn during the function is called.
Thanks for any help.
private void buttonAdd_Click(object sender, EventArgs e)
{
//Adding label to panel
MyLabel label = new MyLabel();
label.Text = "Test";
label.Location = new Point(0, 0);
progressPanel.Controls.Add(label);
//Showing progressPanel
progressPanel.Visible = true;
progressBar1.Minimum = 1;
progressBar1.Value = 1;
progressBar1.Step = 1;
//Some Code
progressPanel.Visible = false;
}
Your problem is obviously that you are performing your task in the UI thread. So the UI itself doesn't get repainted while this task is working. And when you finished, it's still invisible.
Try using a BackgroundWorker instead. Use its ProgressChanged event to update your progress bar.
If you show some code, I can go into details of implementation.
UPDATE:
I actually prefer sstan's answer, but I promised to show the BackgroundWorker way. It may still be helpful if for some reason you cannot use async/await:
public partial class Form1 : Form
{
private BackgroundWorker _backgroundWorker;
public Form1()
{
InitializeComponent();
_backgroundWorker.DoWork += DoWork;
_backgroundWorker.RunWorkerCompleted += WorkerCompleted;
_backgroundWorker.WorkerReportsProgress = true;
_backgroundWorker.ProgressChanged += WorkerProgressed;
}
private void buttonAdd_Click(object sender, EventArgs e)
{
//Adding label to panel
MyLabel label = new MyLabel();
label.Text = "Test";
label.Location = new Point(0, 0);
progressPanel.Controls.Add(label);
//Showing progressPanel
progressPanel.Visible = true;
progressBar1.Minimum = 0;
progressBar1.Maximum = 100;
progressBar1.Value = 0;
progressBar1.Step = 1;
// to avoid multiple starts
buttonAdd.Enabled = false;
// start working
_backgroundWorker.RunWorkerAsync();
}
private void DoWork(object sender, DoWorkEventArgs e)
{
_backgroundWorker.ReportProgress(1);
// work
_backgroundWorker.ReportProgress(50);
// more work
_backgroundWorker.ReportProgress(100);
}
private void WorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
progressPanel.Visible = false;
buttonAdd.Enabled = true;
}
private void WorkerProgress(object sender, ProgressChangedEventArgs e)
{
progressBar1.Value = e.ProgressPercentage;
}
}
So as you see, BackgroundWorkers are fairly easy to use, but async/await is even easier since you don't have to write so much code just for parallelization.
The behavior you see is normal. The changes in visibility only take effect when the UI thread gets a chance to repaint your window. But, while you function is running, the UI thread is busy, so it can't repaint your window. The UI thread only frees up at the end of your function call, so that's when the component's visibility changes take effect.
What you need to do is to perform your function's work on a different thread so that the UI thread is free to repaint your window while the function is still running.
One way to do this, is by using Task.Run() combined with the async/await keywords. Here is a basic example of how this could look like, using the code you posted:
async private void buttonAdd_Click(object sender, EventArgs e)
{
// ....
//Showing progressPanel
progressPanel.Visible = true;
progressBar1.Minimum = 1;
progressBar1.Value = 1;
progressBar1.Step = 1;
// this work will happen on separate thread,
// so the UI thread will be free to update the panel visibility
Progress<int> progress = new Progress<int>(percentage => progressBar1.Value = percentage);
await Task.Run(() => this.WorkToBePerformedOnSeparateThread(progress));
progressPanel.Visible = false;
}
private void WorkToBePerformedOnSeparateThread(IProgress<int> progress)
{
// do work...
progress.Report(25); // Report 25% completed...
// do more work
progress.Report(50); // Report 50% completed...
// more work
progress.Report(75); // Report 75% completed...
// etc...
}
As pointed out by Rene in the comments, just remember that you can only do UI work on the UI thread. So, in the example above, you'll notice that the progress reporting is performed through the Progress<T> class which allows you to change the progress bar value (UI work) from the separate thread, because it takes care of ensuring that the progress reporting happens on the UI thread.

Backgroundworker blocks UI

I try to perform an easy task in an other backgroundthread, so the UI doesn't get blocked, but it still gets blocked. Did I forget anything?
public partial class backgroundWorkerForm : Form
{
public backgroundWorkerForm()
{
InitializeComponent();
}
private void doWorkButton_Click(object sender, EventArgs e)
{
if (backgroundWorker.IsBusy != true)
{
// Start the asynchronous operation.
backgroundWorker.RunWorkerAsync();
}
}
private void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
{
//BackgroundWorker worker = sender as BackgroundWorker;
if (textBoxOutput.InvokeRequired)
{
textBoxOutput.Invoke(new MethodInvoker(delegate
{
for (int i = 0; i < 10000; i++)
{
textBoxOutput.AppendText(i + Environment.NewLine);
}
}));
}
}
}
While the textBox gets filled, the UI is blocked:
Your app wants to repeatedly send updates from the background thread to the UI. There is a built-in mechanism for this: the ProgressChanged event for the background worker. A ReportProgress call is triggered in the background, but executes on the UI thread.
I do change one thing, however. Performance can degrade with too many cross-thread calls. So instead of sending an update every iteration, I instead will batch them into 100.
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
const int maxIterations = 10000;
var progressLimit = 100;
var staging = new List<int>();
for (int i = 0; i < maxIterations; i++)
{
staging.Add(i);
if (staging.Count % progressLimit == 0)
{
// Only send a COPY of the staging list because we
// may continue to modify staging inside this loop.
// There are many ways to do this. Below is just one way.
backgroundWorker1.ReportProgress(staging.Count, staging.ToArray());
staging.Clear();
}
}
// Flush last bit in staging.
if (staging.Count > 0)
{
// We are done with staging here so we can pass it as is.
backgroundWorker1.ReportProgress(staging.Count, staging);
}
}
// The ProgressChanged event is triggered in the background thread
// but actually executes in the UI thread.
private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
if (e.ProgressPercentage == 0) return;
// We don't care if an array or a list was passed.
var updatedIndices = e.UserState as IEnumerable<int>;
var sb = new StringBuilder();
foreach (var index in updatedIndices)
{
sb.Append(index.ToString() + Environment.NewLine);
}
textBoxOutput.Text += sb.ToString();
}
EDIT:
This requires you set the background worker's WorkerReportsProgress property to true.
It's not important that you pass a count with the ReportProgress call. I do so just to have something and to quickly check if I can return.
One really should keep in mind about how many events are being invoked and queued up. Your original app had 10,000 cross thread invocations and 10,000 changed text events for textBoxOutput. My example uses 100 cross thread calls since I use a page size of 100. I could still have generated 10,000 changed text events for the textbox, but instead use a StringBuilder object to hold a full page of changes and then update the textbox once for that page. That way the textbox only has 100 update events.
EDIT 2
Whether or not your app needs paging is not the main deal. The biggest take away should be that the background worker really should use ReportProgress when trying to communicate info back to the UI. See this MSDN Link. Of particular note is this:
You must be careful not to manipulate any user-interface objects in
your DoWork event handler. Instead, communicate to the user interface
through the ProgressChanged and RunWorkerCompleted events.
Your invocation code should be outside the loop. Everything in the invoked codeblock, will be executed on the UI thread, thus blocking it.
private void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
{
//BackgroundWorker worker = sender as BackgroundWorker;
for (int i = 0; i < 10000; i++)
{
// do long-running task
//if (textBoxOutput.InvokeRequired)
//{
textBoxOutput.Invoke(new MethodInvoker(delegate
{
textBoxOutput.AppendText(i + Environment.NewLine);
}));
//}
}
}
an easier way would be to do completely create your output text, and then paste the full output into the TextBox, then you only need one invocation
protected delegate void SetTextDelegate(TextBox tb, string Text);
protected void SetText(TextBox tb, string Text)
{
if (tb.InvokeRequired) {
tb.Invoke(new SetTextDelegate(SetText), tb, Text);
return;
}
tb.Text = Text;
}
and then inside your dowork
private void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
{
StringBuilder sb = new StringBuilder();
//BackgroundWorker worker = sender as BackgroundWorker;
for (int i = 0; i < 10000; i++)
{
sb.AppendLine(i.ToString());
}
SetText(textBoxOutput, sb.ToString());
}

How do I make a repeating button with a thread in c#?

I've looked at some guides and none of them have gotten me all the way there. I've never made a thread, discussed a thread, or seen a thread at the grocery store, so this may be a problem. Currently. I'm trying:
private void btnHUp_MouseDown(object sender, MouseEventArgs e)
{
{
ThreadStart HUp = new ThreadStart(dothis);
t = new Thread(HUp);
t.Start();
}
}
public void dothis()
{
if (intHour < 23)
intHour = intHour += intStep;
lblTimerHour.Text = intHour.ToString("00");
}
private void btnHUp_MouseUp(object sender, MouseEventArgs e)
{
t.Abort();
}
}
That gets me InvalidOperationException was unhandled on the
lblTimerHour.Text = intHour.ToString("00");
line. I read what that means and... it might as well be in Mandarin, I kind of get the general concept-ish of what's going wrong, but it's painfully fuzzy. If you asked me the first step in fixing it I'd look at you like a deer in the headlights. We just haven't gotten that far in my class yet.
The problem here is that the label you are trying to update is owned by the main thread (i.e. what the UI runs on), and that means that only that thread can access/update it. So, since you are in a different thread, you need to tell the UI thread to update the label for you.
Something like this would work:
Action updateLabel = () => lblTimerHour.Text = intHour.ToString("00");
lblTimerHour.BeginInvoke(updateLabel);
What this does is tell the lblTimerHour to invoke the action you define above (updateLabel).
See this post: How to update the GUI from another thread in C#?
lblTimerHour.Invoke((MethodInvoker)delegate {
//Do what you need to do with the label
lblTimerHour.Text = intHour.ToString("00");
});
Edit
This should do the trick:
public void dothis()
{
do
{
if (intHour < 23)
intHour = intHour += intStep;
lblTimerHour.Invoke((MethodInvoker)delegate {
//Update the label from the GUI thread
lblTimerHour.Text = intHour.ToString("00");
});
//Pause 1 sec. Won't freeze the gui since it's in another thread
System.Thread.Sleep(1000);
}while(true); //Thread is killed on mouse up
}
Well, let's take a look and see what you already have.
First, I see you did this.
private void btnHUp_MouseDown(object sender, MouseEventArgs e)
{
ThreadStart HUp = new ThreadStart(dothis);
t = new Thread(HUp);
t.Start();
}
While this certainly is not the freshest stuff around it will still work. If you wanted some fresher ingredients then you might go with this instead.
private void btnHUp_MouseDown(object sender, MouseEventArgs e)
{
Task.Factory.StartNew(dothis);
}
Second, I see this.
public void dothis()
{
if (intHour < 23) intHour = intHour += intStep;
lblTimerHour.Text = intHour.ToString("00");
}
The problem here is that you are attempting to update a UI control from a thread other than the main UI thread. You see UI controls have what is called thread affinity. They can only ever be accessed from the thread that created them. What you have will lead to all kinds of unpredictable problems up to and including tearing a whole in spacetime.
A better option would be to do this.
public void dothis()
{
while (intHour < 23)
{
intHour = intHour += intStep;
lblTimerHour.Invoke((Action)(
() =>
{
lblTimerHour.Text = intHour.ToString("00");
}));
}
}
I assumed that you were missing the loop so I added it. While I cannot say that I personally have a taste for this kind of thing it is much easier to swallow. The real problem here is that the worker thread really does not do a whole lot of useful work. And then to top it off we have to use an awkward marshaling operation to transfer the result back to the UI thread. It is not pretty, but it will work.
And finally that brings me to this.
private void btnHUp_MouseUp(object sender, MouseEventArgs e)
{
t.Abort();
}
You are attempting to abort a thread which is highly inadvisable. The problem is that it yanks control from the thread at unpredictable times. That thread might be in the middle of a write to data structure which would corrupt it. This is actually a pretty bad problem because any data structure in the process of being manipulated from any one of the frames on the call stack could be in an inconsistent state. This includes code you did not write. That is why it is hard to say what you may or may not be corrupting by doing this.
What you need to consider instead is using the cooperative cancellation mechanisms. This includes the use of CancellationTokenSource and CancellationToken. Here is how it might look once we put everything together.
private CancellationTokenSource cts = null;
private void btnHUp_MouseDown(object sender, MouseEventArgs e)
{
cts = new CancellationTokenSource();
Task.Factory.StartNew(() => dothis(cts.Token));
}
private void btnHUp_MouseUp(object sender, MouseEventArgs e)
{
cts.Cancel();
}
public void dothis(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
intHour += intStep;
lblTimerHour.Invoke((Action)(
() =>
{
lblTimerHour.Text = intHour.ToString("00");
}));
Thread.Sleep(1000);
}
}
What this does is signal that the worker thread should gracefully shutdown on its own. This gives the worker thread a chance to tidy things up before eventually terminating itself.
If you want to update the UI every X period of time then there are already existing tools for this; a Timer will do exactly what you want, and it will be much more efficient and easier to code than creating a new thread that just spends most of its time napping. Additionally, aborting threads is a very bad sign to see. Avoid it at all costs.
First create the timer and configure it in the constructor:
private System.Windows.Forms.Timer timer = new System.Windows.Forms.Timer();
private int hour = 0;
private int step = 0;
public Form1()
{
InitializeComponent();
timer.Tick += timer_Tick;
timer.Interval = 1000;
}
Have the Tick event do whatever should be done whenever it ticks.
private void timer_Tick(object sender, EventArgs e)
{
if (hour < 23)
{
hour += step;
lblTimerHour.Text = hour.ToString("00");
}
}
Then just start the timer when you want it to start ticking and stop the timer when you want it to stop:
private void btnHUp_MouseDown(object sender, MouseEventArgs e)
{
timer.Start();
}
private void btnHUp_MouseUp(object sender, MouseEventArgs e)
{
timer.Stop();
}
The timer will automatically ensure that the Tick event handler runs in the UI thread, and it won't block the UI thread (or any other thread) when its waiting for the next event to happen, it will just do nothing.

Instantiating a progress bar that runs asynchronously and modifying it with thread safe calls

In my application I have a queue download list which consists of progress bars and the file names. When the user clicks a button the file name and progress bar is instantiated and added to the queue. Files download one at a time and asynchronously. What I want to do is keep all the progress bars of the files that are waiting to be downloaded yellow in color and then turn green when it is being downloaded and then turn blue when they are completed. It currently works if I have CheckForIllegalCrossThreadCalls = false; in the constructor of the custom progress bar. I want to see if there is a way to make thread safe changes to the progress bars.
I have each queue item set up as an object. The queue item objects are created from the main form code (Form1.cs) when a button is pressed and the progress bars are created in the queue item constructor, which is probably where my problem begins. The downloads are started through a function in the queue item object.
Queue Item Snippet
public class QueueItem
{
public bool inProgress;
public QueueBar bar;
public QueueItem(args)
{
bar = new QueueBar();
inProgress = false;
// handle arguments
}
public void Download()
{
// process info
WebClient client = new WebClient();
client.DownloadFileCompleted += new AsyncCompletedEventHandler(client_DownloadFileCompleted);
client.DownloadProgressChanged += new DownloadProgressChangedEventHandler(client_DownloadProgressChanged);
client.DownloadFileAsync(url, #savePath);
}
private long lastByte = 0;
private long newByte = 0;
private void client_DownloadProgressChanged(object sender, DownloadProgressChangedEventArgs e)
{
percentValue = e.ProgressPercentage;
bar.Value = e.ProgressPercentage;
newByte = e.BytesReceived;
}
private void client_DownloadFileCompleted(object sender, AsyncCompletedEventArgs e)
{
// change bar color
bar.Value = 100;
}
}
Queue Bar Snippet
public class QueueBar : ProgressBar
{
// variables
public QueueBar()
{
this.SetStyle(ControlStyles.UserPaint, true);
// initialize variables
}
// function to change text properties
// function to change color
protected override void OnPaint(PaintEventArgs e)
{
// painting
}
}
Main Function Snippet
public partial class Form1 : Form
{
private List<QueueItem> qItems;
private BackgroundWorker queue;
private void button_Click(object sender, EventArgs e)
{
// basic gist of it
qItems.Add(new QueueItem(args));
Label tmpLabel = new Label();
tmpLabel.Text = filename;
tmpLabel.Dock = DockStyle.Bottm;
splitContainerQueue.Panel2.Controls.Add(tmpLabel);
splitContainerQueue.Panel2.Controls.Add(qItems[qItems.Count - 1].bar);
if (!queue.IsBusy) { queue.RunWorkerAsync(); }
}
private void queue_DoWork(object sender, DoWorkEventArgs e)
{
while (qItems.Count > 0)
{
if (!qItems[0].inProgress && qItems[0].percentValue == 0)
{
qItems[0].inProgress = true;
qItems[0].Download();
}
// else if statements
}
}
I also just tried creating a background worker to create the Queue Items and add the controls asynchronously but that doesn't work since the split container was created on a different thread.
You cannot call a UI control (created on your UI thread) from another thread safely - you need to use InvokeRequired / BeginInvoke() for such calls. When calling BeginInvoke() you'll pass a delegate; something like this (just some sample code, yours will look slightly different):
private void SomeEventHandler ( object oSender, EventArgs oE )
{
if ( InvokeRequired )
{
MethodInvoker oDelegate = (MethodInvoker) delegate
{
SomeEventHandler ( oSender, oE );
};
BeginInvoke ( oDelegate );
return;
}
else
{
// already on the correct thread; access UI controls here
}
}
You also cannot create your progress bars away from the UI thread - you need to create all your controls as part of your UI and then if you need to access these progress bars from your queue items, you'll have to pass in a reference to the progress bar. When you try to access the progress bar, you'll do
if ( bar.InvokeRequired ) { ... }
to determine if you're trying to call it from the right thread.
The reason for this mess is because controls handle many of their property updates through messages and those messages must be delivered synchronously, in the correct order. The only way to ensure this (without some very complex coding) is to create all controls on the same thread where the thread runs a message pump.

How to update GUI with backgroundworker?

I have spent the whole day trying to make my application use threads but with no luck. I have read much documentation about it and I still get lots of errors, so I hope you can help me.
I have one big time consuming method which calls the database and updates the GUI. This has to happen all the time(or about every 30 seconds).
public class UpdateController
{
private UserController _userController;
public UpdateController(LoginController loginController, UserController userController)
{
_userController = userController;
loginController.LoginEvent += Update;
}
public void Update()
{
BackgroundWorker backgroundWorker = new BackgroundWorker();
while(true)
{
backgroundWorker.DoWork += new DoWorkEventHandler(backgroundWorker_DoWork);
backgroundWorker.RunWorkerAsync();
}
}
public void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
{
_userController.UpdateUsersOnMap();
}
}
With this approach I get an exception because the backgroundworker is not and STA thread(but from what I can understand this is what I should use). I have tried with a STA thread and that gave other errors.
I think the problem is because I try to update the GUI while doing the database call(in the background thread). I should only be doing the database call and then somehow it should switch back to the main thread. After the main thread has executed it should go back to the background thread and so on. But I can't see how to do that.
The application should update the GUI right after the database call. Firering events don't seem to work. The backgroundthread just enters them.
EDIT:
Some really great answers :) This is the new code:
public class UpdateController{
private UserController _userController;
private BackgroundWorker _backgroundWorker;
public UpdateController(LoginController loginController, UserController userController)
{
_userController = userController;
loginController.LoginEvent += Update;
_backgroundWorker = new BackgroundWorker();
_backgroundWorker.DoWork += backgroundWorker_DoWork;
_backgroundWorker.RunWorkerCompleted += backgroundWorker_RunWorkerCompleted;
}
public void _backgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
_userController.UpdateUsersOnMap();
}
public void Update()
{
_backgroundWorker.RunWorkerAsync();
}
void backgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
//UI update
System.Threading.Thread.Sleep(10000);
Update();
}
public void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
{
// Big database task
}
}
But how can I make this run every 10 second? System.Threading.Thread.Sleep(10000) will just make my GUI freeze and while(true) loop in Update() as suggested gives an exception(Thread too busy).
You need to declare and configure the BackgroundWorker once - then Invoke the RunWorkerAsync method within your loop...
public class UpdateController
{
private UserController _userController;
private BackgroundWorker _backgroundWorker;
public UpdateController(LoginController loginController, UserController userController)
{
_userController = userController;
loginController.LoginEvent += Update;
_backgroundWorker = new BackgroundWorker();
_backgroundWorker.DoWork += new DoWorkEventHandler(backgroundWorker_DoWork);
_backgroundWorker.ProgressChanged += new ProgressChangedEventHandler(backgroundWorker_ProgressChanged);
_backgroundWorker.WorkerReportsProgress= true;
}
public void Update()
{
_backgroundWorker.RunWorkerAsync();
}
public void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
{
while (true)
{
// Do the long-duration work here, and optionally
// send the update back to the UI thread...
int p = 0;// set your progress if appropriate
object param = "something"; // use this to pass any additional parameter back to the UI
_backgroundWorker.ReportProgress(p, param);
}
}
// This event handler updates the UI
private void backgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
// Update the UI here
// _userController.UpdateUsersOnMap();
}
}
You have to use the Control.InvokeRequired property to determine if you are on a background thread. Then you need to invoke your logic that modified your UI via the Control.Invoke method to force your UI operations to occur on the main thread. You do this by creating a delegate and passing it to the Control.Invoke method. The catch here is you need some object derived from Control to call these methods.
Edit: As another user posted, if yo you can wait to the BackgroundWorker.Completed event to update your UI then you can subscribe to that event and call your UI code directly. BackgroundWorker_Completed is called on the main app thread. my code assumes you want to do updates during the operation. One alternative to my method is to subscribe to the BwackgroundWorker.ProgressChanged event, but I believe you'll need to still call Invoke to update your UI in that case.
for example
public class UpdateController
{
private UserController _userController;
BackgroundWorker backgroundWorker = new BackgroundWorker();
public UpdateController(LoginController loginController, UserController userController)
{
_userController = userController;
loginController.LoginEvent += Update;
}
public void Update()
{
// The while loop was unecessary here
backgroundWorker.DoWork += new DoWorkEventHandler(backgroundWorker_DoWork);
backgroundWorker.RunWorkerAsync();
}
public delegate void DoUIWorkHandler();
public void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
{
// You must check here if your are executing on a background thread.
// UI operations are only allowed on the main application thread
if (someControlOnMyForm.InvokeRequired)
{
// This is how you force your logic to be called on the main
// application thread
someControlOnMyForm.Invoke(new
DoUIWorkHandler(_userController.UpdateUsersOnMap);
}
else
{
_userController.UpdateUsersOnMap()
}
}
}
You should remove the while(true), you are adding infinite event handlers and invoking them infinite times.
You can use the RunWorkerCompleted event on the backgroundWorker class to define what should be done when the background task has completed. So you should do the database call in the DoWork handler, and then update the interface in the RunWorkerCompleted handler, something like this:
BackgroundWorker bgw = new BackgroundWorker();
bgw.DoWork += (o, e) => { longRunningTask(); }
bgw.RunWorkerCompleted += (o, e) => {
if(e.Error == null && !e.Cancelled)
{
_userController.UpdateUsersOnMap();
}
}
bgw.RunWorkerAsync();
In addition to previous comments, take a look at www.albahari.com/threading - best doc on threading you will ever find. It will teach you how to use the BackgroundWorker properly.
You should update the GUI when the BackgroundWorker fires Completed event (which is invoked on UI thread to make it easy for you, so that you don't have to do Control.Invoke yourself).
Here's a source code pattern you can use based on some WinForms example code, but you can apply it for WPF as well very easily. In this example, I am redirecting output to a Console which I then use to let the background worker write some messages to a textbox while it is processing.
It consists of:
A helper class TextBoxStreamWriter used to redirect console output to a textbox
A background worker writing to the redirected console
A progress bar which needs to be reset after completion of background worker
Some text boxes (txtPath and txtResult), and a "Start" button
In other words, there is some background task which needs to interact with the UI. Now I am going to show how that is done.
From the context of the background task, you need to use Invoke to access any UI element. I believe the simplest way to do that is to use lambda expression syntax, like
progressBar1.Invoke((Action) (() =>
{ // inside this context, you can safely access the control
progressBar1.Style = ProgressBarStyle.Continuous;
}));
To update the ProgressBar, a local method like
private void UpdateProgress(int value)
{
progressBar1.Invoke((Action)(() => { progressBar1.Value = value; }));
}
helps. It is passing the value parameter to the progress bar as a closure.
This is the helper class TextBoxStreamWriter, which is used to redirect console output:
public class TextBoxStreamWriter : TextWriter
{
TextBox _output = null;
public TextBoxStreamWriter(TextBox output)
{
_output = output;
}
public override void WriteLine(string value)
{
// When character data is written, append it to the text box.
// using Invoke so it works in a different thread as well
_output.Invoke((Action)(() => _output.AppendText(value+"\r\n")));
}
}
You need to use it in the form load event as follows (where txtResult is a textbox, to which the output will be redirected):
private void Form1_Load(object sender, EventArgs e)
{
// Instantiate the writer and redirect the console out
var _writer = new TextBoxStreamWriter(txtResult);
Console.SetOut(_writer);
}
There is also a button on the form which starts the background worker, it passes a path to it:
private void btnStart_Click(object sender, EventArgs e)
{
backgroundWorker1.RunWorkerAsync(txtPath.Text);
}
This is the workload of the background worker, note how it uses the console to output messages to the textbox (because of the redirection that was set up earlier):
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
var selectedPath = e.Argument as string;
Console.Out.WriteLine("Processing Path:"+selectedPath);
// ...
}
The variable selectedPath consists of the path that was passed to the backgroundWorker1 earlier via the parameter txtPath.Text, it is being accessed via e.Argument.
If you need to reset some controls afterwards, do it in the following way (as already mentioned above):
private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
progressBar1.Invoke((Action) (() =>
{
progressBar1.MarqueeAnimationSpeed = 0;
progressBar1.Style = ProgressBarStyle.Continuous;
}));
}
In this example, after completion, a progress bar is being reset.
Important: Whenever you access a GUI control, use Invoke as I did in the examples above.
Using Lambda's makes it easy, as you could see in the code.
And here's the complete example, which runs in LinqPad 6 (just copy and paste it into an empty C# Program query) - I decided to use LinqPad this time so you can learn something new, because you all know how to create a new Windows Forms project in Visual Studio (and if you still want to do so, just copy the events below and drag and drop the controls to the form):
// see: https://stackoverflow.com/a/27566468/1016343
using System.ComponentModel;
using System.Windows.Forms;
BackgroundWorker backgroundWorker1 = new System.ComponentModel.BackgroundWorker();
ProgressBar progressBar1 = new ProgressBar() { Text = "Progress", Width = 250, Height=20, Top=10, Left=0 };
TextBox txtPath = new TextBox() { Text =#"C:\temp\", Width = 100, Height=20, Top=30, Left=0 };
TextBox txtResult = new TextBox() { Text = "", Width = 200, Height=250, Top=70, Left=0, Multiline=true, Enabled=false };
Button btnStart = new Button() { Text = "Start", Width = 100, Height=30, Top=320, Left=0 };
void Main()
{
// see: https://www.linqpad.net/CustomVisualizers.aspx
// Instantiate the writer and redirect the console out
var _writer = new TextBoxStreamWriter(txtResult);
Console.SetOut(_writer);
// wire up events
btnStart.Click += (object sender, EventArgs e) => btnStart_Click(sender, e);
backgroundWorker1.DoWork += (object sender, DoWorkEventArgs e) => backgroundWorker1_DoWork(sender, e);
backgroundWorker1.RunWorkerCompleted += (object sender, RunWorkerCompletedEventArgs e)
=> backgroundWorker1_RunWorkerCompleted(sender, e);
using var frm = new Form() {Text="Form", Width = 300, Height=400, Top=0, Left=0};
frm.Controls.Add(progressBar1);
frm.Controls.Add(txtPath);
frm.Controls.Add(txtResult);
frm.Controls.Add(btnStart);
// display controls
frm.ShowDialog();
}
private void btnStart_Click(object sender, EventArgs e)
{
backgroundWorker1.RunWorkerAsync(txtPath.Text);
}
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
InitProgress();
var selectedPath = e.Argument as string;
Console.Out.WriteLine("Processing Path: " + selectedPath);
UpdateProgress(0); Thread.Sleep(300); UpdateProgress(30); Thread.Sleep(300);
UpdateProgress(50); Thread.Sleep(300);
Console.Out.WriteLine("Done.");
// ...
}
private void UpdateProgress(int value)
{
progressBar1.Invoke((Action)(() =>
{
progressBar1.Value = value;
}));
}
private void InitProgress()
{
progressBar1.Invoke((Action)(() =>
{
progressBar1.MarqueeAnimationSpeed = 0;
progressBar1.Style = ProgressBarStyle.Continuous;
}));
}
private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
UpdateProgress(100); // always show 100% when done
}
// You can define other methods, fields, classes and namespaces here
public class TextBoxStreamWriter : TextWriter
{
TextBox _output = null;
public TextBoxStreamWriter(TextBox output)
{
_output = output;
}
public override Encoding Encoding => throw new NotImplementedException();
public override void WriteLine(string value)
{
// When character data is written, append it to the text box.
// using Invoke so it works in a different thread as well
_output.Invoke((Action)(() => _output.AppendText(value + "\r\n")));
}
}
The if-statement in #Lee's answer should look like:
bgw.RunWorkerCompleted += (o, e) => {
if(e.Error == null && !e.Cancelled)
{
_userController.UpdateUsersOnMap();
}
}
...if you want to invoke UpdateUsersOnMap(); when there are no errors and BgWorker hasn't been cancelled.

Categories