I am using a background worker to analyze serial port data on a pop-up Windows form in my application. As part of error-proofing my application I want to be able to stop/cancel the test script if the user tries to close the form during operation. Currently I have my backgroundworker setup as follows:
private void backgroundworker_DoWork(object sender, DoWorkEventArgs e)
{
LogMsg(LogMsgType.Normal, "Test Started");
// execute test script
}
If I try to close the form I get a "Invoke or BeginInvoke cannot be called on a control until the window handle has been created" exception at my LogMsg() portion of code:
public void LogMsg(LogMsgType msgtype, string msgIn)
{
rtbAssignSATest.Invoke(new EventHandler(delegate
{
rtbAssignSATest.SelectedText = string.Empty;
rtbAssignSATest.SelectionFont = new Font(rtb.SelectionFont, FontStyle.Bold);
rtbAssignSATest.SelectionColor = _mainForm.LogMsgTypeColor[(int)msgtype];
rtbAssignSATest.AppendText(msgIn);
rtbAssignSATest.ScrollToCaret();
}));
}
With this in mind I evaluated other posts on StackOverflow on how to cancel a thread when closing a windows form. In doing so I came across a portion of code that I have incorporated into my application (see below).
volatile bool mClosePending = false;
volatile bool mCompleted = false;
private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
mCompleted = true;
if (mClosePending)
{
this.Close();
}
}
protected override void OnFormClosing(FormClosingEventArgs e)
{
if (!mCompleted)
{
backgroundWorker1.CancelAsync();
this.Enabled = false;
e.Cancel = true;
mClosePending = true;
return;
}
base.OnFormClosing(e);
}
Even after incorporating this code I am still seeing the error at the LogMsg() portion of code. Any help would be appreciated.
Let the base.OnFormClosing(e) to execute even after you set the e.Cancel = true. I think this will work.
protected override void OnFormClosing(FormClosingEventArgs e)
{
e.Cancel = !mCompleted;
base.OnFormClosing(e)
if (!mCompleted)
{
backgroundWorker1.CancelAsync();
this.Enabled = false;
mClosePending = true;
}
}
Could you put your
base.OnFormClosing(e);
line at the top of the method? Just in case it is overwriting your
e.Cancel = true;
Edit, also, instead of using volatile, why don't you use
lock (this) {}
When do you start the background worker? It could be before the controls on the Form have been loaded - this would explain the error message...
Related
I have a question regarding looping with button click event, I've tried many methods & searched many pages in search for a simple answer for the past hour, but the truth is each answer just looks like alien code, probably because I'm still very new to developing.
Here's a simplified version of what I'm trying to do :
private string Message = "Hello";
private void Spam(bool loop)
{
if (loop == true)
{
while (loop == true)
{
MessageBox.Show(Message);
}
}
else { MessageBox.Show("Spamming has stopped !! "); }
}
private void button1_Click(object sender, EventArgs e)
{
Spam(true);
}
private void button2_Click(object sender, EventArgs e)
{
Spam(false);
}
Obviously this isn't my API, or it'd be a useless thing to invent, however, the code itself is long & you guys always ask for "relevant code" (No disrespect), so there it is.
My problem : Breaking out of the spam loop upon clicking button 2, the code to me looks decent enough for the API to figure out, but each time button 1 is clicked, the API freezes.
Use a background worker to do your work. You can use the cancellation feature to break out of it when you're done. Your loop as you have it will block the UI thread when executed syncronously, which is why your GUI becomes unresponsive. Note if you do any interaction with the UI in the do work delegate, you need to marshal back onto the UI thread (via invoke for example).
private BackgroundWorker _worker = null;
private void goButton_Click(object sender, EventArgs e)
{
_worker = new BackgroundWorker();
_worker.WorkerSupportsCancellation = true;
_worker.DoWork += new DoWorkEventHandler((state, args) =>
{
do
{
if (_worker.CancellationPending)
break;
Console.WriteLine("Hello, world");
} while (true);
});
_worker.RunWorkerAsync();
goButton.Enabled = false;
stopButton.Enabled = true;
}
private void stopButton_Click(object sender, EventArgs e)
{
stopButton.Enabled = false;
goButton.Enabled = true;
_worker.CancelAsync();
}
Update 2019:
BackgroundWorker is now largely obsolete, replaced by the async/await feature in later versions of C# which is easier to use. Here is an example of how to achieve the same thing using that feature:
private CancellationTokenSource _canceller;
private async void goButton_Click(object sender, EventArgs e)
{
goButton.Enabled = false;
stopButton.Enabled = true;
_canceller = new CancellationTokenSource();
await Task.Run(() =>
{
do
{
Console.WriteLine("Hello, world");
if (_canceller.Token.IsCancellationRequested)
break;
} while (true);
});
_canceller.Dispose();
goButton.Enabled = true;
stopButton.Enabled = false;
}
private void stopButton_Click(object sender, EventArgs e)
{
_canceller.Cancel();
}
There's one important thing to remember:
While your code is being executed, the user cannot interact with your user interface.
That means: You first need to exit the loop (i.e. return from the Spam method), and then the user can click Button2.
That's a hard truth, because it means you cannot write the code in the way you wanted to. Fortunately, there are a few ways to work around that:
Don't use a loop. Use some kind of timer to do the "spamming". Button1 starts the timer, Button2 stops it. What kind of timer is available depends on the user interface library you use (WinForms has a Timer, WPF has a DispatcherTimer).
Do the "spamming" in a background thread. This will allow your user interface to stay responsive, and you can communicate with the background thread, for example, by setting a volatile Boolean. This, however, is an advanced topic (and can quickly lead to complex synchronization issues), so I suggest that you try the other option first.
When you click button1 the Spam method is called and loop is starting. When you click button2 Spam method is called but it's not the same. It's the second execution, so it will check the condition and won't enter into the loop, but the loop in the first call sill will be running.
You should use a flag and the loop should use that flag to determine whether it should be still running. It should look something like that:
bool run = false;
string message = "This API is not original";
private void Spam()
{
while (run == true)
{
MessageBox.Show(message);
}
}
}
private void button1_Click(object sender, EventArgs e)
{
message = "Hellooo";
flag = true;
Spam();
}
private void button2_Click(object sender, EventArgs e)
{
flag = false;
}
Take a look at this concept:
private bool loop = false;
private void Start()
{
loop = true;
Spam("Some Message??");
}
private void Spam(string message)
{
while (loop)
{
MessageBox.Show("This API is not original");
}
}
private void button1_Click(object sender, EventArgs e)
{
loop = true;
}
private void button2_Click(object sender, EventArgs e)
{
loop = false;
}
However, the user won't be able to press a button if a MessageBox keeps popping up as it takes up the main UI thread. In order to prevent this you could use BackgroundWorker or start a new thread.
I have an application in which I launch a window that displays byte data coming in from a 3rd party tool. I have included .CancelAsync() and .CancellationPending into my code (see below) but I have another issue that I am running into.
private void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
{
Thread popupwindow = new Thread(() => test());
popupwindow.Start(); // start test script
if(backgroundWorker.CancellationPending == true)
{
e.Cancel = true;
}
}
private voide window_FormClosing(object sender, FormClosingEventArgs e)
{
try
{
this.backgroundWorker.CancelAsync();
}
catch (Exception ex)
{
MessageBox.Show(ex.Message.ToString());
}
}
Upon cancelling the test I get an `InvalidOperationException occurred" error from my rich text box in my pop-up window. It states that "Invoke or BeginInvoke" cannot be called on a control until the window handle has been created". I am not entirely sure what that means and would appreciate your help.
LogWindow code for Rich Text Box:
public void LogWindowText(LogMsgType msgtype, string msgIn)
{
rtbSerialNumberValue.Invoke(new EventHandler(delegate
{
rtbWindow.SelectedText = string.Empty;
rtbWindow.SelectionFont = new Font(rtbWindow.SelectionFont, FontStyle.Bold);
rtbWindow.SelectionColor = LogMsgTypeColor[(int)msgtype];
rtbWindow.AppendText(msgIn);
rtbWindow.ScrollToCaret();
}));
}
After reading your code; it appears the background worker completes nearly instantly; likely killing any threads that were spawned from it. More to the point; a background worker that is already stopped will throw "InvalidOperationException" when "CancelAsync" is called.
I'd advise placing any GUI related work into caller instead of the background worker thread. This is an important consideration because you will get cross-thread exceptions and other strange behavior such as rather serious GUI refresh issues.
The background worker "DoWork" method should be considered threaded already. You can see this by adding simple debug statements to your code.
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
Debug.WriteLine(Thread.CurrentThread.ManagedThreadId);
}
private void button1_Click(object sender, EventArgs e)
{
Debug.WriteLine(Thread.CurrentThread.ManagedThreadId);
backgroundWorker1.RunWorkerAsync();
}
Finally; I'd add that CancellationPending works best when polled in a loop-construct like so,
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
foreach (var workItem in work)
{
workItem.Perform();
if (backgroundWorker1.CancellationPending)
{
break;
}
}
}
Hope this helps.
What's happening is the window handle is already gone before the LogWindowText method is called by the finalize method (RunWorkerCompleted handler) of the background worker. You need to check that Handle:
if (this.Handle == IntPtr.Zero) { return; }
I am making and app using C# and Winforms that archives and saves folders to specified locations,for archiving folders i have a BackgroundWorker which takes as input a folder path and generates a zip archive.Now in the next step the file needs to be moved at specified location,again as the file is large enough and could hang up UI thread i moved the code to another BackgroundWorker named FileMove,everything works well except that the FileMove is not reporting any progress,here is the function that i call as soon as archiving is over;
private void FileMove_DoWork(object sender, DoWorkEventArgs e)
{
label3.Text = "Saving file,please wait...";
File.Move(temppath + #"\Output.jpg", savefilename);
}
private void FileMove_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
label3.Text = "Saving file,please wait... " + e.ProgressPercentage.ToString(); //This should show Progress Percentage but it doesn't.
}
private void FileMove_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
label3.Text = ("The folder has been successfully hidden.");
button1.Enabled = true;
button2.Enabled = true;
button3.Enabled = true;
this.ControlBox = true;
}
The problem i'm facing is as soon as file moving starts label3 shows "Saving file,please wait..." and after a long time(as i'm compressing 900-1000 MB)it shows "The folder has been successfully hidden.".During ProgressChanged event label should also show Percentage but it doesn't.Please point out or correct where i've gone wrong.Any help will be appreciated.
First, your BackgroundWorker is trying to update the UI from its background thread, which is a no-no in multithreaded UI apps.
To update the UI you'll need it to switch to the UI thread to do the update. This is done by first checking if the label3.InvokeRequired is true (indicating you can't update the UI from the current thread), then passing a delegate to label3.Invoke (not Delegate.Invoke())
This is a pattern you need to be very familiar with for WinForms development. The Control.Invoke MSDN page includes a sample of leveraging this pattern.
Second, you need to call BackgroundWorker.ReportProgress() periodically, which fires the ProgressChanged event with a percentage-complete value.
Here's a sample app. It assumes you've got a Form with a Button, a Label, and a BackgroundWorker with WorkgerReportsProgress = true (and WorkerSupportsCancellation = true, for bonus points).
When you click the button it starts a 5-second blocking task, then switches to 10 1-second blocking tasks (reporting progress along the way).
Note the helper method InvokeIfRequired() that ensures the UI is updated from the correct thread. There's no harm in using this excessively.
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
if (backgroundWorker1.IsBusy)
{
label1.Text = "Reset!";
backgroundWorker1.CancelAsync();
return;
}
backgroundWorker1.RunWorkerAsync();
}
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
label1.InvokeIfRequired(() => label1.Text = "Gettin busy");
System.Threading.Thread.Sleep(5000);
for (int i = 0; i < 10; i++)
{
backgroundWorker1.ReportProgress(i*10);
System.Threading.Thread.Sleep(1000);
}
}
private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
label1.InvokeIfRequired(() => label1.Text = "Done");
}
private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
label1.InvokeIfRequired(() => label1.Text = string.Format("{0}% done", e.ProgressPercentage));
}
}
public static class InvokeExtensions
{
public static void InvokeIfRequired(this ISynchronizeInvoke control, MethodInvoker action)
{
if (control.InvokeRequired)
{
control.Invoke(action, null);
}
else
{
action();
}
}
}
I want to do something after the document have completely load... I dont want to use WebBrowser.DocumentCompleted Event, so please don't suggest me this.
I tried 2 ways to do it but they not work. Can someone tell me what I doing wrong?
Example 1
wb.Navigate("http://www.google.com");
while(wb.ReadyState != WebBrowserReadyState.Complete) { }
richtextdocument.Text = wb.DocumentText;
Example 2
wb.Navigate("http://www.google.com");
while(wb.isBusy == true) { }
richtextdocument.Text = wb.DocumentText;
Try to use timer to validate document load state.
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void timer1_Tick(object sender, EventArgs e)
{
if(webBrowser1.ReadyState == WebBrowserReadyState.Complete)
{
timer1.Enabled = false;
richTextBox1.Text = webBrowser1.DocumentText;
}
}
private void button1_Click(object sender, EventArgs e)
{
timer1.Enabled = true;
webBrowser1.Navigate("http://www.google.com");
}
}
What you are dealing with here is trying to call an inherently asynchronous method synchronously.
As you mentioned in the comments to your question that the reason for not using DocumentCompleted is that you will need to use that event for other purposes, what I suggest you do is to use the DocumentCompleted event, coupled with a private class boolean flag to determine if this is the special case of DocumentCompleted or not.
private bool wbNeedsSpecialAction; //when you need to call the special case of Navigate() set this flag to true
public Form1()
{
InitializeComponent();
wb.DocumentCompleted += new WebBrowserDocumentCompletedEventHandler(wb_DocumentCompleted);
}
void wb_DocumentCompleted(object sender, WebBrowserDocumentCompletedEventArgs e)
{
if (wbNeedsSpecialAction)
{
richtextdocument.Text = wb.DocumentText;
wbNeedsSpecialAction = false;
}
else
{
//other cases of using DocumentCompleted...
}
}
public void Browse()
{
wbNeedsSpecialAction = true; //make sure the event is treated differently
wb.Navigate("http://www.google.com");
}
This will still allow you to control other cases within the event handler.
You must take special care to ensure that your user is not able to trigger another call to Navigate() before this 'special action' page finishes loading, otherwise it may steal the special case event. One way could be to block the UI until the page finishes loading, e.g.:
Cursor.Current = Cursors.WaitCursor;
while(wb.ReadyState != WebBrowserReadyState.Complete) {application.doevents }
I'd like to use a BackgroundWorker for background operations because I thought there is no need to take care of "BeginInvoke" etc. when updating WinForm-Controls. Is that right? As far as I know, you can update WinForms controls directly by using the ProgressChanged and RunWorkerCompleted eventhandlers.
But I can't, I although get the following exception:
Control control name accessed from a thread other than the thread it was created on
Some code:
public partial class ConfigurationForm : Form
{
public ConfigurationForm()
{
InitializeComponent();
backgroundWorker1.WorkerReportsProgress = true;
backgroundWorker1.WorkerSupportsCancellation = true;
label1.Text = String.Empty;
// [...]
}
private void StartButton_Click(object sender, EventArgs e)
{
if (backgroundWorker1.IsBusy != true)
{
label1.Text = "Converting...";
backgroundWorker1.RunWorkerAsync();
}
}
private void CancelButton_Click(object sender, EventArgs e)
{
if (backgroundWorker1.WorkerSupportsCancellation == true)
{
backgroundWorker1.CancelAsync();
}
progressBar1.Dispose();
this.Close();
}
private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
progressBar1.Value = e.ProgressPercentage;
// EXCEPTION here, why?
}
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
BackgroundWorker worker = sender as BackgroundWorker;
Converter c = new Converter();
c.Start(worker, e);
}
private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
// EXCEPTION in all cases, why?
if (e.Cancelled == true)
{
label1.Text = "Canceled";
}
else if (e.Error != null)
{
label1.Text = "Error: " + e.Error.Message;
}
else
{
label1.Text = "Done!";
}
}
}
I have to say, this is not a WinForms application but an VSTO PowerPoint add-in. The Form above gets created by the add-in like this when the user is clicking an icon in the ribbon bar of PowerPoint:
//Do I need [STAThread] here? but doesn't seem to work anyway
private void button1_Click(object sender, RibbonControlEventArgs e)
{
ConfigurationForm config = new ConfigurationForm();
config.Show();
}
Can you tell me what's the problem here?
I posted the link but I don't actually think it is the best solution. Clearly the failure occurs because you never called Application.Run() or used Form.ShowDialog(). You can assign the context explicitly as shown but you can get some very tricky problems if you don't do it right. Like assigning it more than once.
The better fix is to ask it to automatically install itself. Which then ensures that whatever form you create will then install it only when it wasn't done before. Put this in front of the form creation code:
WindowsFormsSynchronizationContext.AutoInstall = true;
With the big advantage that if the code ever gets repeated, you won't create another instance of it and potentially screw up the thread's ExecutionContext.
Do consider ShowDialog() as another fix. If I'm not mistaken then you now also have a problem with tabbing and shortcut keystrokes.
Your assumption would be correct for Windows Forms. The way it works though is BackgroundWorkers uses the SynchronizationContext of the current thread. In a windows app, that will be a WindowsFormsSynchronizationContext, which does the marshalling for you.
In a VSTO app, it won't be. It will probably just be the default one, which simply executes the methods. The link from Hans Passant has the code you need to get it to work as expected. I.e.:
System.Threading.SynchronizationContext.SetSynchronizationContext(new WindowsFormsSynchronizationContext());
...create and start your background worker here...