Search code examples
c#multithreadingbackgroundworkerargument-passingcancel-button

Passing argument into backgroundWorker (for use as a Cancel button)


I'm new to C# and object-oriented programming in general. I've been trying to implement a "Cancel" button into my GUI so that the user can stop it mid-process.

I read this question: How to implement a Stop/Cancel button? and determined that a backgroundWorker should be a good option for me, but the example given doesn't explain how to hand arguments to the backgroundWorker.

My problem is that I do not know how to pass an argument into backgroundWorker such that it will stop the process; I have only been able to get backgroundWorker to stop itself.

I created the following code to try to learn this, where my form has two buttons (buttonStart and buttonStop) and a backgroundWorker (backgroundWorkerStopCheck):

using System;
using System.ComponentModel;
using System.Windows.Forms;
using System.Threading;
using System.Timers;

namespace TestBackgroundWorker
{
    public partial class Form1 : Form
    {
        public Form1()
        {         
            InitializeComponent();

            // Set the background worker to allow the user to stop the process. 
            backgroundWorkerStopCheck.WorkerSupportsCancellation = true;
        }

        private System.Timers.Timer myTimer;

        private void backgroundWorkerStopCheck_DoWork(object sender, DoWorkEventArgs e)
        {
            //If cancellation is pending, cancel work.  
            if (backgroundWorkerStopCheck.CancellationPending)
            {
                e.Cancel = true;
                return;
            }
        }

        private void buttonStart_Click(object sender, EventArgs e)
        {
            // Notify the backgroundWorker that the process is starting.
            backgroundWorkerStopCheck.RunWorkerAsync();
            LaunchCode();
        }

        private void buttonStop_Click(object sender, EventArgs e)
        {
            // Tell the backgroundWorker to stop process.
            backgroundWorkerStopCheck.CancelAsync();
        }

        private void LaunchCode()
        {
            buttonStart.Enabled = false; // Disable the start button to show that the process is ongoing.
            myTimer = new System.Timers.Timer(5000); // Waste five seconds.
            myTimer.Elapsed += new ElapsedEventHandler(myTimer_Elapsed);
            myTimer.Enabled = true; // Start the timer.
        }

        void myTimer_Elapsed(object sender, ElapsedEventArgs e)
        {
            buttonStart.Enabled = true; // ReEnable the Start button to show that the process either finished or was cancelled.
        }
    }
}

The code, if it worked properly, would just sit there for five seconds after the user clicked "Start" before re-enabling the Start button, or would quickly reactivate the Start button if the user clicked "Stop".

There are two problems with this code that I am not sure how to handle:

1) The "myTimer_Elapsed" method results in an InvalidOperationException when it attempts to enable the Start button, because the "cross-thread operation was not valid". How do I avoid cross-thread operations?

2) Right now the backgroundWorker doesn't accomplish anything because I don't know how to feed arguments to it such that, when it is canceled, it will stop the timer.

I'd appreciate any assistance!


Solution

  • First of all, the problem to avoid "cross-thread operation was not valid" is use Invoke on controls. You cannot use a control from a different thread.

    About the second issue, I would implement it in the following way. This is a minimum background worker implementation with cancel support.

    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Text;
    using System.Windows.Forms;
    
    namespace WindowsFormsApplication5
    {
        public partial class Form1 : Form
        {
            public Form1()
            {
                InitializeComponent();
    
                // Set the background worker to allow the user to stop the process. 
                backgroundWorkerStopCheck.WorkerSupportsCancellation = true;
                backgroundWorkerStopCheck.DoWork += new DoWorkEventHandler(backgroundWorkerStopCheck_DoWork);
            }
    
            private void backgroundWorkerStopCheck_DoWork(object sender, DoWorkEventArgs e)
            {
                try
                {
                    for (int i = 0; i < 50; i++)
                    {
                        if (backgroundWorkerStopCheck.CancellationPending)
                        {
                            // user cancel request
                            e.Cancel = true;
                            return;
                        }
    
                        System.Threading.Thread.Sleep(100);
                    }
                }
                finally
                {
                    InvokeEnableStartButton();
                }
            }
    
            private void buttonStart_Click(object sender, EventArgs e)
            {
                //disable start button before launch work
                buttonStart.Enabled = false;
    
                // start worker
                backgroundWorkerStopCheck.RunWorkerAsync();
            }
    
            private void buttonStop_Click(object sender, EventArgs e)
            {
                // Tell the backgroundWorker to stop process.
                backgroundWorkerStopCheck.CancelAsync();
            }
    
            private void InvokeEnableStartButton()
            {
                // this method is called from a thread,
                // we need to Invoke to avoid "cross thread exception"
                if (this.InvokeRequired)
                {
                    this.Invoke(new EnableStartButtonDelegate(EnableStartButton));
                }
                else
                {
                    EnableStartButton();
                }
            }
    
            private void EnableStartButton()
            {
                buttonStart.Enabled = true;
            }
        }
    
        internal delegate void EnableStartButtonDelegate();
    }
    

    About passing arguments to the worker, you can pass any object in the RunWorkerAsync() method, and its reveived in the backgroundWorkerStopCheck_DoWork method:

      ...
      backgroundWorkerStopCheck.RunWorkerAsync("hello");
      ...
    
      private void backgroundWorkerStopCheck_DoWork(object sender, DoWorkEventArgs e)
      {
          string argument = e.Argument as string;
          // argument value is "hello"
          ...
      }
    

    Hope it helps.