0

I'm working on writing a class which is derived from the System.ComponentModel.BackgroundWorker class. The reason I am doing so in my project is that I need a lot of information to be returned in different types of status update events, depending on which event is raised. When attempting to update any of the controls the main form from any of my update events, I am getting the following error:

System.InvalidOperationException: 'Cross-thread operation not valid: Control '' accessed from a thread other than the thread it was created on.'

The first control that I am attempting to update is a ToolStripStatusLabel, which does not have an .Invoke() method. I have created minimally verifiable example below. To recreate the error, simply create a new Windows Forms App (.NET Framework) project targeted to .NET 4.8 and copy paste the following code into the Form1.cs file:

using System;
using System.ComponentModel;
using System.Windows.Forms;


namespace WindowsFormsApp1
{
    public partial class Form1 : Form
    {
        private StatusStrip statusStrip1;
        private ToolStripStatusLabel toolStripStatusLabel1;
        private ToolStripProgressBar toolStripProgressBar1;
        private Button button1;
        private MyBGW myBGW;

        public Form1()
        {
            InitializeComponent();
            this.statusStrip1 = new StatusStrip();
            this.toolStripStatusLabel1 = new ToolStripStatusLabel() { Text = "Starting Text" };
            this.toolStripProgressBar1 = new ToolStripProgressBar();
            this.button1 = new Button();
            this.myBGW = new MyBGW();

            this.statusStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {this.toolStripStatusLabel1, this.toolStripProgressBar1});
            this.Controls.Add(this.statusStrip1);
            this.Controls.Add(this.button1);

            this.button1.Click += Button1_Click;
            this.myBGW.OnMyBGW_StatusChanged += MyBGW_OnMyBGW_StatusChanged;
        }

        private void Button1_Click(object sender, EventArgs e) { myBGW.RunWorkerAsync(); }

        private void MyBGW_OnMyBGW_StatusChanged(object sender, MyBGW.MyBGW_StatusChanged_EventArgs e)
        {
            // The following two lines will throw the cross-threading exception
            this.toolStripStatusLabel1.Text = e.StatusText;
            if (e.PBarStyle != MyBGW.pBarStyles.NoChange) { this.toolStripProgressBar1.Style = (ProgressBarStyle)e.PBarStyle; }
        }

    }

    public class MyBGW : BackgroundWorker
    {
        public enum pBarStyles { Block = 0, Continuous = 1, Marquee = 2, NoChange = -1 }
        public delegate void MyBGW_StatusChanged_EventHandler(object sender, MyBGW_StatusChanged_EventArgs e);
        public event MyBGW_StatusChanged_EventHandler OnMyBGW_StatusChanged;

        public class MyBGW_StatusChanged_EventArgs : EventArgs
        {
            public string StatusText;
            public pBarStyles PBarStyle;
            public MyBGW_StatusChanged_EventArgs(string statusText, pBarStyles pBarStyle)
            {
                this.StatusText = statusText; this.PBarStyle = pBarStyle;
            }
        }

        public new void RunWorkerAsync() { base.RunWorkerAsync(); }

        private void myBGW_DoWork(object sender, DoWorkEventArgs e)
        {
            OnMyBGW_StatusChanged(this, new MyBGW_StatusChanged_EventArgs(DateTime.Now.ToString(), pBarStyles.Marquee));
            System.Threading.Thread.Sleep(10000);
            OnMyBGW_StatusChanged(this, new MyBGW_StatusChanged_EventArgs("Done", pBarStyles.Continuous));
        }

        public MyBGW() { base.DoWork += new DoWorkEventHandler(this.myBGW_DoWork); }
    }
}

My best guess is that I am raising or consuming the event incorrectly which is causing the code to still be run on the worker thread instead of the main/UI thread, but I'm coming up short in my research on what I'm missing.

EDIT: this question is not related to Cross-thread operation not valid: Control accessed from a thread other than the thread it was created on as it is not directly relying on a BackgroundWorker but is rather attempting to add additional events to a derived class, of which the addition of those events are causing the Cross-Thread exception. Also, the answer does not apply as the control attempting to be updated does not have the .Invoke method as the solution to that question stated.

The problem for this question is in relation to how the event was being raised, which was incorrectly, causing the consumption of that event to be on the wrong thread and raising the cross-thread exception.

  • https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.backgroundworker.dowork?view=net-5.0 `You must be careful not to manipulate any user-interface objects in your DoWork event handler.` – mjwills Jul 12 '21 at 05:33
  • @mjwills, The user interface objects are not being modified in the `.DoWork` handler. The data is being sent in an `EventArg` when raising a new event from the `.DoWork` method. The raised event is then being consumed by a method that I am under the impression should be running on the UI thread. – вʀaᴎᴅᴏƞ вєнᴎєƞ Jul 12 '21 at 05:39
  • So, just to clarify, you are claiming that this line of code `this.toolStripStatusLabel1.Text = e.StatusText;` is _not_ being invoked from `DoWork`? I am not sure how to respond to that. I mean, it clearly is... You can step through it and see for yourself (it isn't going to magically jump to the UI thread without you telling it to). You may want to check the source code for https://referencesource.microsoft.com/#system/compmod/system/componentmodel/BackgroundWorker.cs,5c09078822addc69,references to compare to yours. – mjwills Jul 12 '21 at 06:10
  • Is there a particular reason you've used inheritance rather than composition for `MyBGW`? – mjwills Jul 12 '21 at 06:20
  • Then, as stated at the end of the question, I am raising the event incorrectly/not at all. My understanding is that when the event `OnMyBGW_StatusChanged` is called that raises the event in which is then consumed by `MyBGW_OnMyBGW_StatusChanged` method in the Form1 class because that method is subscribed from the line `this.myBGW.OnMyBGW_StatusChanged += MyBGW_OnMyBGW_StatusChanged;`. I chose to inherit because I need the ability to raise several types of events and only using the included `ProgressChanged` would be cumbersome. – вʀaᴎᴅᴏƞ вєнᴎєƞ Jul 12 '21 at 06:25
  • `I am raising the event incorrectly` Yes, that is correct. https://stackoverflow.com/a/1698918/34092 – mjwills Jul 12 '21 at 06:27
  • As a side note, the `BackgroundWorker` is no longer the coolest tool in the toolbox. After the advent of the async/await technology, it is now considered a [technologically obsolete](https://stackoverflow.com/questions/12414601/async-await-vs-backgroundworker/64620920#64620920) class. – Theodor Zoulias Jul 12 '21 at 11:42

3 Answers3

1

The BackgroundWorker.DoWork event handler is supposed to do background work, and it's not intended for interacting with the UI. This handler is invoked on a ThreadPool thread, and interacting with UI components from any thread other than the UI thread is not allowed. The BackgroundWorker class offers two events that are raised on the UI thread¹, the ProgressChanged and the RunWorkerCompleted. You could take advantage of this, by invoking your StatusChanged event on the ProgressChanged event handler (or overriding the OnProgressChanged method), and passing your StatusChangedEventArgs as an argument of the ReportProgress method:

public class MyBGW : BackgroundWorker
{
    public enum BarStyles { Block = 0, Continuous = 1, Marquee = 2, NoChange = -1 }
    public delegate void StatusChangedEventHandler(object sender,
        StatusChangedEventArgs e);
    public event StatusChangedEventHandler StatusChanged;

    public MyBGW() { this.WorkerReportsProgress = true; }

    public class StatusChangedEventArgs : EventArgs
    {
        public string StatusText;
        public BarStyles PBarStyle;
        public StatusChangedEventArgs(string statusText, BarStyles pBarStyle)
        {
            this.StatusText = statusText; this.PBarStyle = pBarStyle;
        }
    }

    protected override void OnDoWork(DoWorkEventArgs e)
    {
        this.ReportProgress(-1,
            new StatusChangedEventArgs(DateTime.Now.ToString(), BarStyles.Marquee));
        base.OnDoWork(e);
        this.ReportProgress(-1,
            new StatusChangedEventArgs("Done", BarStyles.Continuous));
    }

    protected override void OnProgressChanged(ProgressChangedEventArgs e)
    {
        if (e.ProgressPercentage == -1 && e.UserState is StatusChangedEventArgs args)
            StatusChanged?.Invoke(this, args);
        else
            base.OnProgressChanged(e);
    }
}

¹ To be precise, the ProgressChanged and RunWorkerCompleted events are raised on the SynchronizationContext.Current which is captured when the BackgroundWorker.RunWorkerAsync is invoked.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
0

Because toolStripStatusLabel1 And toolStripProgressBar1 runs inside a thread other than the main thread, it needs to be Invoke. And since ToolStripStatusLabel And ToolStripProgressBar itself does not have an Invoke method, we use its parent Invoke method.

change MyBGW_OnMyBGW_StatusChanged to :

private void MyBGW_OnMyBGW_StatusChanged(object sender, MyBGW.MyBGW_StatusChanged_EventArgs e)
{
    InvokeIfRequired(this, ()=>
    {
       this.toolStripStatusLabel1.Text = e.StatusText;
    });
            
    if (e.PBarStyle != MyBGW.pBarStyles.NoChange) 
    {
        InvokeIfRequired(this, () =>
        {
            this.toolStripProgressBar1.Style = (ProgressBarStyle)e.PBarStyle;
        });
    }
}
       

add InvokeIfRequired method

public void InvokeIfRequired(Control control, MethodInvoker action)
{
    if (control.InvokeRequired)
       control.Invoke(action);
    else
       action();
}
Meysam Asadi
  • 6,438
  • 3
  • 7
  • 17
0

As mjwills has stated in the comments of the question, I was not raising the event properly, which was causing the event to be consumed on the same worker thread. After looking at the link for the .NET source code of the BackgroundWorker class, I can see that there is a bit of code, AsyncOperation.Post() that has the method protected virtual void OnStatusChangedin the code below raised in the main thread rather than the worker thread.

public class MyBGW : BackgroundWorker
{
    public enum pBarStyles { Block = 0, Continuous = 1, Marquee = 2, NoChange = -1 }
    private static readonly object statusChangedKey = new object();
    private AsyncOperation asyncOperation = null;
    public MyBGW() { base.DoWork += new DoWorkEventHandler(this.myBGW_DoWork); }

    public delegate void StatusChanged_EventHandler(object sender, StatusChanged_EventArgs e);

    public event StatusChanged_EventHandler StatusChanged
    {
        add { this.Events.AddHandler(statusChangedKey, value); }
        remove { this.Events.RemoveHandler(statusChangedKey, value); }
    }

    protected virtual void OnStatusChanged(StatusChanged_EventArgs e) { ((StatusChanged_EventHandler)Events[statusChangedKey])?.Invoke(this, e); }

    private void StatusReporter(object arg) { OnStatusChanged((StatusChanged_EventArgs)arg); }

    public void UpdateStatus(StatusChanged_EventArgs e) { asyncOperation.Post(new System.Threading.SendOrPostCallback(StatusReporter), e); }

    public class StatusChanged_EventArgs : EventArgs
    {
        public string StatusText;
        public pBarStyles PBarStyle;
        public StatusChanged_EventArgs(string statusText, pBarStyles pBarStyle)
        {
            this.StatusText = statusText; this.PBarStyle = pBarStyle;
        }
    }

    public new void RunWorkerAsync() { asyncOperation = AsyncOperationManager.CreateOperation(null); base.RunWorkerAsync(); }

    private void myBGW_DoWork(object sender, DoWorkEventArgs e)
    {
        UpdateStatus(new StatusChanged_EventArgs(DateTime.Now.ToString(), pBarStyles.Marquee));
        System.Threading.Thread.Sleep(3000);
        UpdateStatus(new StatusChanged_EventArgs("Done", pBarStyles.Continuous));
    }
}

I don't fully understand the how and why, but it works. Hopefully someone can comment below with a better explanation.