For all too long, I have been trying to run an external .bat file (calls an R script for some statistical processing), and have the console redirect to the U.I.
I think I am close, but just as I have gotten it to work I have run into a sizable problem! That is: it only bloody works once the main thread has ended (via: return;), and not during Thread.Sleep, or .WaitOne() or etc.
Here is my code in the main thread.
string batLoc = ALLRG___.RSCRBIN_LOC + "current.bat";
BackgroundWorker watchboxdWorker1 = new BackgroundWorker();
watchboxdWorker1.DoWork += frmC.WatchboxWorker1_WatchExt;
frmC.wbResetEvent = new AutoResetEvent(false);
watchboxdWorker1.RunWorkerAsync(batLoc);
//Thread.Sleep(1000*20);
//frmC.wbResetEvent.WaitOne();
return;
Note the commented out Sleep and/or WaitOne() instructions. If I try and use these the BackgroundWorker DOES execute, but the 'events' which update the U.I do not.
The code in my form (frmC above) is as follows,
public void WatchboxWorker1_WatchExt(object sender, DoWorkEventArgs e)
{
string exeLoc = (string) e.Argument;
string arg1 = exeLoc;
string arg2 = "";
ProcessStartInfo pStartInfo = new ProcessStartInfo();
pStartInfo.FileName = exeLoc;
pStartInfo.Arguments = string.Format("\"{0}\" \"{1}\"", arg1, arg2);
pStartInfo.WorkingDirectory = Path.GetDirectoryName(exeLoc);
pStartInfo.CreateNoWindow = true;
pStartInfo.UseShellExecute = false;
pStartInfo.RedirectStandardInput = true;
pStartInfo.RedirectStandardOutput = true;
pStartInfo.RedirectStandardError = true;
Process process1 = new Process();
process1.EnableRaisingEvents = true;
process1.OutputDataReceived += new DataReceivedEventHandler(wbOutputHandler);
process1.ErrorDataReceived += new DataReceivedEventHandler(wbErrorHandler);
process1.StartInfo = pStartInfo;
process1.SynchronizingObject = rtbWatchbox;
process1.Start();
process1.BeginOutputReadLine();
process1.BeginErrorReadLine();
process1.StandardInput.Close();
process1.WaitForExit();
wbResetEvent.Set();
}
public void wbOutputHandler(Object source, DataReceivedEventArgs outLine)
{
int x = 0;
if (!String.IsNullOrEmpty(outLine.Data))
{
rtbWatchbox.AppendText(outLine.Data);
}
}
public void wbErrorHandler(Object source, DataReceivedEventArgs outLine)
{
int x = 0;
if (!String.IsNullOrEmpty(outLine.Data))
{
rtbWatchbox.AppendText(outLine.Data);
}
}
My problem is --
The wbOutputHandler and wbErrorHandler get fired as the console updates nicely - but only when the main thread has exited (using the return;).... if I use the Thread.Sleep or .WaitOne() in the main thread to pass control to the BackgroundWorker (WatchboxWorker1_WatchExt), then the code runs successfully, but the wbOutputHandler and wbErrorHandler methods do not get triggered at all.
In fact, if I do the Thread.Sleep(10*1000), then the external program starts running as planned, 10 seconds pass, then when the main UI thread exits I get a whole big enormous update all at once.
I don't want to have my main thread closed, I want to keep doing stuff there after the Worker is finished!
[ of course happy for alternate methods that are a better approach ]
"Help me Stack Overflow, you are my only hope!"
The answer was to put a backgroundWorker within another backgroundWorker, which is created for the UI Thread. I thought quite complicated given the reletivly simple requirement of printing a console output to the UI!
I now call my functions from the UI as follows -
private void btInsertBCModls_Click(object sender, EventArgs e)
{
BackgroundWorker bw = new BackgroundWorker();
bw.DoWork += RC2___Scratchpad4.BC_RunExistingBCModel;
bw.RunWorkerAsync(this);
}
Next I use the delegate & Invoke method on any richTextBox I need to update from another thread -
delegate void UpdateWriteboxCallback(String str);
public void wbWriteBox(string WriteString)
{
if (!String.IsNullOrEmpty(WriteString))
{
if (rtbWatchbox.InvokeRequired)
{
UpdateWriteboxCallback at = new UpdateWriteboxCallback(wbWriteBox);
this.Invoke(at, new object[] { WriteString });
}
else
{
// append richtextbox as required
}
}
}
Then from within my function I use another BackgroundWorker to run the console stuff -
public static void BC_RunExistingBCModel(object sender, DoWorkEventArgs e)
{
RC2___RhinegoldCoreForm frmC = e.Argument as RC2___RhinegoldCoreForm;
BackgroundWorker watchboxWorker = new BackgroundWorker();
watchboxWorker.DoWork += frmC.WatchboxWorker_RunProc;
watchboxWorker.RunWorkerAsync(batLoc);
while (watchboxWorker.IsBusy)
Thread.Sleep(50);
frmC.UpdateRGCoreStatusBox4("Executed script " + m + "... ");
}
Which in turn, in the DoWork function, calls the wbWriteBox function above.
public void WatchboxWorker_RunProc(object sender, DoWorkEventArgs e)
{
string exeLoc = (string) e.Argument;
string arg1 = exeLoc;
string arg2 = "";
ProcessStartInfo pStartInfo = new ProcessStartInfo();
pStartInfo.FileName = exeLoc;
pStartInfo.Arguments = string.Format("\"{0}\" \"{1}\"", arg1, arg2);
pStartInfo.WorkingDirectory = Path.GetDirectoryName(exeLoc);
pStartInfo.CreateNoWindow = true;
pStartInfo.UseShellExecute = false;
pStartInfo.RedirectStandardInput = true;
pStartInfo.RedirectStandardOutput = true;
pStartInfo.RedirectStandardError = true;
Process process1 = new Process();
process1.EnableRaisingEvents = true;
process1.OutputDataReceived += (s, e1) => this.wbWriteBox(e1.Data);
process1.ErrorDataReceived += (s, e1) => this.wbWriteBox(e1.Data);
process1.StartInfo = pStartInfo;
process1.SynchronizingObject = rtbWatchbox;
process1.Start();
process1.BeginOutputReadLine();
process1.BeginErrorReadLine();
process1.StandardInput.Close();
process1.WaitForExit();
//wbResetEvent.Set();
}
Phew! A tricky solution to an easily defined problem. If someone has a better way, let me know. And thanks to Carsten for all the help - magnificent.