Search code examples
c#multithreadingprocessbackgroundworker

Events in BackgroundWorker thread not firing while main thread active


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!"


Solution

  • 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.