0

I have a WindowsForm in C# with a TrayIcon including a contextMenuStrip with a ToolStripTextBox, a FileSystemWatcher and a BackgroundWorker.

The FileSystemWatcher throws an event when a new file is created which then starts the BackgroundWorker. The BackgroundWorker reports progress which updates a ToolStripTextBox on the TrayIcon in the ProgressChanged event handler.

Curiously this works fine as long as the ToolStripMenu has not been visible since the start of the program. As soon as I right-click on the TrayIcon to show the ToolStripMenu (regardless of the BackgroundWorker being idle or not), the ToolStripTextBox starts to throw an InvalidOperationException (invalid cross-thread operation) every time I try to update its .Text property. System.InvalidOperationException is thrown here

When I start the BackgroundWorker from a button click event it all works fine, too. I can see the ToolStripTextBox updating.

What is different when I start the BackgroundWorker from the FileSystemWatcher event? Or rather what is different after showing the contextMenuStrip? Does the contextMenuStrip belong to another thread after showing?

I might find another way to show the progress instead of the ToolStripTextBox but I am curious to know what causes this. I'd be very glad if you could help.

Minimal code example below.


public partial class Form1 : Form
{
    FileSystemWatcher watcher = new FileSystemWatcher();
    string watcherpath = @"C:\Temp\files";
    BackgroundWorker bgw = new BackgroundWorker();
    public Form1()
    {
        InitializeComponent();
    }

    private void Form1_Load(object sender, EventArgs e)
    {
        watcher.Path = watcherpath;
        watcher.Created += Watcher_Created;
        watcher.EnableRaisingEvents = true;
        bgw = new BackgroundWorker();
        bgw.DoWork += Bgw_DoWork;
        bgw.RunWorkerCompleted += Bgw_RunWorkerCompleted;
        bgw.WorkerSupportsCancellation = true;
        bgw.WorkerReportsProgress = true;
        bgw.ProgressChanged += Bgw_ProgressChanged;
    }

    private void Bgw_ProgressChanged(object sender, ProgressChangedEventArgs e)
    {
        toolStripTextBox1.Text = $"Progress: {e.ProgressPercentage}%";
    }

    private void Bgw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        MessageBox.Show("Done!");
    }

    private void Bgw_DoWork(object sender, DoWorkEventArgs e)
    {
        for (int i = 1; i <= 10; i++)
        {
            Thread.Sleep(1000);
            (sender as BackgroundWorker).ReportProgress(i * 10);
        }
    }

    private void Watcher_Created(object sender, FileSystemEventArgs e)
    {
        File.Delete(e.FullPath);
        bgw.RunWorkerAsync();
    }

    private void toolStripMenuItem1_Click(object sender, EventArgs e)
    {
        this.Close();
    }

    private void notifyIcon1_MouseClick(object sender, MouseEventArgs e)
    {
        if (e.Button == MouseButtons.Right)
            contextMenuStrip1.Show(Cursor.Position);
    }

    private void button1_Click(object sender, EventArgs e)
    {
        bgw.RunWorkerAsync();
    }
}***
dee7kay
  • 23
  • 6
  • your trying to change a control from a thread which it was not created from try and use `Control.Invoke()` – Lucifer Jan 08 '20 at 13:11
  • Does this answer your question? [How do I update the GUI from another thread?](https://stackoverflow.com/questions/661561/how-do-i-update-the-gui-from-another-thread) – Lucifer Jan 08 '20 at 13:12
  • 2
    BackgroundWorker has no real cue which specific thread is the anointed UI thread in a program. It has to take a guess, and does so by paying attention which thread called RunWorkerAsync(). That's a problem in this program, it is not the UI thread that calls it. The events raised by FileSystemWatcher run on a threadpool thread. Simplest way to fix it is to add `watcher.SynchronizingObject = this;` in the form constructor. Now it raises its events on the UI thread. You still have a problem with RunWorkerAsync() getting called before the worker is done, but that's another issue. – Hans Passant Jan 08 '20 at 13:17
  • 1
    @LinQuini Yes, it solves the problem with the cross thread call. But I still don't understand what is changed when the ContextMenustrip.Show() method is called. It works fine until then and shows the last update (e.g. 40%) when I click but on the next iteration (that would be 50%) the exception is thrown. What is causing this? – dee7kay Jan 08 '20 at 13:33
  • @HansPassant Why does the BackgroundWorker update toolStripTextBox fine until the contextMenuStrip1.Show() method is called the first time? Does it "forget" which one is the UI thread then? Btw: I know about bgw.IsBusy but I omitted it here for shortness. – dee7kay Jan 08 '20 at 13:51
  • 2
    Because a toolstrip item that is not visible yet does not have to do anything to update visible text. So the Text assignment can't trigger the exception yet, it merely sets a string variable. Once it is visible then a lot more happens to make the text visible on the screen. Now it matters a wholeheckofalot which thread is doing the job. It must be the UI thread. – Hans Passant Jan 08 '20 at 13:57
  • Follow-up question: I can update the NotifyIcon.Text and NotifyIcon.Icon from the BGW / FilesystemWatcher thread without any problem, too. What is different with this Control? Does it not belong to the UI thread? – dee7kay Jan 08 '20 at 13:58
  • 2
    NotifyIcon is a very different animal, it is not a Control and not a ToolStripItem. It is a component that wraps an operating system object. Owned by Explorer, the process that owns the notification area. In effect it always gets called from the wrong thread. So it knows how to deal with that. – Hans Passant Jan 08 '20 at 14:01
  • 1
    Thank you very much! I solved my (specific) problem by adding watcher.SynchronizingObject = this; as proposed by @HansPassant. Seems like I learned something today. :D – dee7kay Jan 08 '20 at 14:30

1 Answers1

0

Please try below code

    private void Bgw_ProgressChanged(object sender, ProgressChangedEventArgs e)
    {
         SetToolStripText(e.ProgressPercentage)
    }

    private void SetToolStripText(object ProgressPercentage)
    {
            this.BeginInvoke((MethodInvoker)delegate { this.toolStripTextBox1.Text = $"Progress: {e.ProgressPercentage}%";
    }
Lucifer
  • 1,594
  • 2
  • 18
  • 32
  • 2
    Please, see Hans Passant's comment: without a SynchronizationObject, the BackGroundWorker events are generated in the Thread that created the BGW. When that is set, you don't need to invoke anymore. You should also use `BeginInvoke()` here, not `Invoke()`, so you could also remove the `InvokeRequired` check. You *could*... – Jimi Jan 08 '20 at 13:31
  • 1
    The other important matter (already mentioned in the comment) is that the BGW could be called again in the FSW event handler while it's still running and nothing is checking whether `IsBusy`, nor there's currently a way to wait untill it's not (this of course cannot be done in the handler itself, since it cannot be blocked, so you need a proxy method) (btw, please, don't *sir* me, I'm just a colleague :) – Jimi Jan 08 '20 at 13:36
  • ToolStripTextBox has no definition for InvokeRequired or .Invoke(...). But it works this way: this.Invoke((MethodInvoker)delegate { this.toolStripTextBox1.Text = $"Progress: {e.ProgressPercentage}%"; }); – dee7kay Jan 08 '20 at 13:39