I am very new to C# and trying to understand background workers.
Currently when I run this code it will stop redirecting and reading the output from the command prompt after the StopButton has been clicked and leaves the "cancelled" message behind, but then does nothing afterwards. I currently may be implementing this all wrong, but I have the e.Cancel set by clicking the stop button that calls CancelAsync(), which turns CancellationPending = true. Anyone have any ideas how I should go about this?
Thanks so much!! I appreciate the help!
public Form2()
{
InitializeComponent();
bw.WorkerReportsProgress = true;
bw.WorkerSupportsCancellation = true;
bw.DoWork += new DoWorkEventHandler(bw_DoWork);
bw.RunWorkerCompleted += new
RunWorkerCompletedEventHandler(bw_RunWorkerCompleted);
}
private void StopButton_Click(object sender, EventArgs e)
{
if (bw.WorkerSupportsCancellation == true)
{
bw.CancelAsync();
bw.Dispose();
Invoke(new ToDoDelegate(() => this.textBox2.Text += Environment.NewLine + "Cancelled " + Environment.NewLine));
}
}
private void Submit_Click(object sender, EventArgs e)
{
if(bw.IsBusy != true)
{
bw.RunWorkerAsync();
Invoke(new ToDoDelegate(() => this.textBox2.Text += "Starting Download " + Environment.NewLine));
}
}
public bool DoSVNCheckout(String KeyUrl, DoWorkEventArgs e)
{
SVNProcess = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "cmd.exe",
Arguments = "/C plink download files using svn"
Verb = "runas",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = false,
}
};
SVNProcess.Start();
while(!SVNProcess.StandardOutput.EndOfStream & bw.CancellationPending == false)
{
string output = SVNProcess.StandardOutput.ReadLine();
Invoke(new ToDoDelegate(() => this.textBox2.Text += output));
}
while (!SVNProcess.StandardError.EndOfStream & bw.CancellationPending == false)
{
string Erroutput = SVNProcess.StandardError.ReadLine();
Invoke(new ToDoDelegate(() => this.textBox2.Text += Erroutput));
}
if(SVNProcess.StandardError.EndOfStream & bw.CancellationPending == false)
{
string Erroutput = SVNProcess.StandardError.ReadLine();
Invoke(new ToDoDelegate(() => this.textBox2.Text += Erroutput));
}
//if I manually close the cmd.exe window by clicking X
//in the top right corner the program runs correctly
//at these lines of code right here
if(bw.CancellationPending == true)
{
e.Cancel = true;
return true;
}
return false;
}
private delegate void ToDoDelegate();
private void bw_DoWork(object sender, DoWorkEventArgs e)
{
BackgroundWorker worker = sender as BackgroundWorker;
if(bw.CancellationPending == true)
{
e.Cancel = true;
return;
}
e.Cancel = DoSVNCheckout(URL, e);
}
private void bw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if ((e.Cancelled == true))
{
this.textBox2.Text = "Canceled!";
}
else{
this.textBox2.Text = "Done!";
}
}
There are a number of problems with the code you've written. The two primary issues, in my opinion are:
BackgroundWorker
operation with the separately-running process you've started. The two are not by any means the same, or even related to each other. Cancelling theBackgroundWorker
won't have any direct effect on the process you start. Your question is not clear on what the actual desired behavior is here, but you don't do anything to actually terminate the external process. At best, if you weren't blocking theDoWork
method waiting for the process to produce output, you'd abandon the process. As it is, without terminating the process, yourDoWork
can never get to a point where it notices you've tried to cancel it, because it's stuck on aReadLine()
call.StandardOutput
andStandardError
streams in serial fashion, i.e. one after the other. The documentation clearly warns against this, as this is a very reliable way to deadlock your code. The buffers for each stream are relatively small, and the entire external process will hang if it tries to write to one of these streams when the buffer is full. This will in turn cause no more output to be written to any of the streams. In your code example, if theStandardError
stream buffer gets full before you have been able to completely read theStandardOutput
stream, the external process will hang and your own process will as well.Another minor issue is that you don't take advantage of the
BackgroundWorker.ProgressChanged
event, which you could have used to pass text you've read from the output and error strings back to the UI thread where you could add that text to your text box. The use ofControl.Invoke()
here isn't strictly necessary, and fails to fully take advantage of the features inBackgroundWorker
.There are ways to modify the code you've written, so that you can still use
BackgroundWorker
and achieve your goal. One obvious improvement that you could make would be to store theProcess
object reference in an instance field, so that it's accessible by theStopButton_Click()
method. In that method, you could call theProcess.Kill()
method to actually terminate the process.But even so, you would need to fix the deadlock-prone implementation you've got now. This can be done in a variety of ways: use the
Process.OutputDataReceived
andProcess.ErrorDataReceived
events; create a secondBackgroundWorker
task to process one of the streams; use theTask
-based idioms to read the streams.The last option there would be my preference. The second option needlessly creates long-running threads, while the event-based pattern (the first option) is awkward to use (and is line-based, so has limited value when dealing with processes that write partial lines during the course of their operation). But if you're going to use the
Task
-based idioms for reading the streams, it seems to me you should upgrade the entire implementation to do so.BackgroundWorker
is still a viable class to use if one wants to, but the newTask
features in conjunction with theasync
/await
keywords offer what is IMHO a much easier and cleaner way to handle asynchronous operations. One of the biggest advantages is that it doesn't rely on explicitly used threads (e.g. running theDoWork
event handler in a thread pool thread). Asynchronous I/O operations such as what makes up the entirety of your scenario here, are handled implicitly via the API, allowing all of the code you write to be executed in the UI thread where you want it.Here is a version of your example that does just this:
Notes:
BackgroundWorker
at all. Theasync
/await
pattern implicitly does all of the same work thatBackgroundWorker
would have for you, but without all the extra boilerplate code required to set it up and manage it._cancelTask
, which represents a simpleTask
object that can be completed. It's only ever completed by being cancelled, in this case, but that's not strictly required…you'll note that theawait
statement that monitors the task completion doesn't actually care how the task ended. Just that it did. In more complex scenarios, one might actually want to use theResult
for such aTask
object, callingSetResult()
to complete the task with a value, and usingSetCanceled()
for actually cancelling the operation being represented. It all depends on the specific context.button1_Click()
method (equivalent to yourSubmit_Click()
method) is written as if everything happens synchronously. Through the "magic" of theawait
statement, the method actually executes in two parts. When you click the button, everything up to theawait
statement executes; at theawait
, once theRunProcess()
method has returned, thebutton1_Click()
method returns. It will resume execution later, when theTask
object returned byRunProcess()
completes, which in turn will happen when that method reaches its end (i.e. not the first time it returns).button1_Click()
method, the UI is updated to reflect the current state of operation: the start button is disabled and the cancel button is enabled. Before returning, the buttons are returned to their original state.button1_Click()
method is also where the_cancelTask
object is created and later discarded. Theawait RunProcess()
statement will see aTaskCanceledException
if theRunProcess()
throws it; this is used to present the user with theMessageBox
reporting that the operation had been cancelled. You can of course respond to an exception like this however you see fit.button2_Click()
method (equivalent to yourStopButton_Click()
method) only has to set the_cancelTask
object to a completed state, in this case by callingSetCanceled()
.RunProcess()
method is where the main handling of the process occurs. It starts the process, then waits for the relevant tasks to complete. The two tasks representing the output and error streams are wrapping in a call toTask.WhenAll()
. This creates a newTask
object that will complete only when all of the wrapped tasks are completed. Then the method waits viaTask.WhenAny()
for that wrapper task and the_cancelTask
object. If either completes, the method will complete execution. If the completed task was the_cancelTask
object, the method does so by killing the process that was started (interrupting it in the middle of whatever it was doing), waiting for the processs to actually exit (which can be observed by the completion of the wrapper task…these complete when the end of both the output and the error streams is reached, which in turn happens when the process has exited), and then throwing theTaskCanceledException
.ConsumeReader()
method is a helper method that simple reads text from the givenTextReader
object, and appends the output to the text box. It usesTextReader.ReadAsync()
; this type of method can also be written usingTextReader.ReadLineAsync()
, but in that case you will only see output at the end of each line. UsingReadAsync()
ensures that output is retrieved as soon as it's available, without waiting for newline characters.RunProcess()
andConsumeReader()
methods are alsoasync
, and also haveawait
statements in them. As withbutton1_Click()
, these methods initially return from executing upon reaching theawait
statement, resuming execution later when the awaitedTask
completes. In theConsumeReader()
example, you'll also notice thatawait
unpacks theint
value that is theResult
property value of theTask<int>
it was waiting on. Theawait
statement forms an expression that evaluates to the waitedTask
'sResult
value.await
in each of these cases is that the framework resumes execution of the method on the UI thread. This is why thebutton1_Click()
method can still access the UI objectsbutton1
andbutton2
after theawait
, and whyConsumeReader()
can access thetextBox1
object for the purpose of appending text each time some is returned by theReadAsync()
method.I realize the above might be a lot to digest. Especially when most of it relates to the complete change from the use of
BackgroundWorker
to theTask
-based API, rather than addressing the main two issues I mentioned at the outset. But I hope you can see how those issues are implicitly addressed by these changes, as well as how other requirements of the code are met in a simpler, easier-to-read fashion by using the modernasync
/await
pattern.For completeness, here is the Designer-generated code that goes along with the above
Form1
class: