6

I have a WinForm with a backgroundWorker:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using SeoTools.Utils;

namespace SeoTools.UI
{
    public partial class UIProgress : Form
    {
        public UIProgress(DoWorkEventHandler doWorkEventHandler, RunWorkerCompletedEventHandler runWorkerCompletedEventHandler)
        {
            InitializeComponent();
            this.backgroundWorker.WorkerReportsProgress = true;
            this.backgroundWorker.WorkerSupportsCancellation = true;
            this.backgroundWorker.DoWork += doWorkEventHandler;
            this.backgroundWorker.RunWorkerCompleted += runWorkerCompletedEventHandler;
        }

        public void Start()
        {
            var foo = SynchronizationContext.Current;
            backgroundWorker.RunWorkerAsync();
        }

        private void btnStop_Click(object sender, EventArgs e)
        {
            btnStop.Enabled = false;
            btnStop.Text = "Stopping...";
            backgroundWorker.CancelAsync(); 
        }

       private void backgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
    {
        try
        {
            wdgProgressBar.Value = e.ProgressPercentage;
            if (this.Visible == false)
            {
                this.ShowDialog();
                this.Update();
            }
        }
        catch (InvalidOperationException) {} 
    }

        private void backgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            this.Hide(); //Here I get a InvalidOperationException
            this.Dispose();
        }    
    }
}

First time I run this it works fine. But second time I get InvalidOperationException when calling this.Hide().

"Additional information: Cross-thread operation not valid: Control 'UIProgress' accessed from a thread other than the thread it was created on."

The weird thing is on first run foo in Start() is a WindowsFormsSyncronizationContext but on the second try it's a System.Threading.SyncronizationContext.

The application I'm writing is a ExcelDna plugin.

EDIT

Start() is called like this:

 UIProgress uiProgress = new UIProgress(
                delegate(object sender, DoWorkEventArgs args)
                {
                   ....
                },
                delegate(object sender, RunWorkerCompletedEventArgs args)
                    {
                       ...
                    }
            );
            uiProgress.Start();
Niels Bosma
  • 11,758
  • 29
  • 89
  • 148
  • How `Start` is invoked? – kennyzx Dec 07 '14 at 15:23
  • 1
    I have found an old [post](http://blogs.msdn.com/b/mattdotson/archive/2006/02/13/531315.aspx) on SyncronizationContext, the technique, is you can save the `WindowsFormsSyncronizationContext` for later use. Don't know how it is switched to another `SyncronizationContext` though, maybe is the Excel-DNA environment...takes some hard debugging time. – kennyzx Dec 08 '14 at 13:41
  • Same as kennyzx.. here is another post - http://blogs.msdn.com/b/kaelr/archive/2007/09/05/synchronizationcallback.aspx I guess this has something to do with how Excel-DNA thing works (never used or even heard of it before.. but looks cool) – Vikas Gupta Dec 09 '14 at 23:49
  • The best option is to use the begininvoke in the callback – Rafa Dec 16 '14 at 15:26

6 Answers6

7

Your Start() method must be called from code that runs on the UI thread to allow the BackgroundWorker to operate correctly. It was not when you get this exception. Add protective code to your method so you can diagnose this mishap:

    public void Start()
    {
        if (Thread.CurrentThread.GetApartmentState() != ApartmentState.STA) {
            throw new InvalidOperationException("Bug! Code called from a worker thread");
        }
        backgroundWorker.RunWorkerAsync();
    }

Now you can set a breakpoint on the throw statement and use the debugger's Call Stack window to find out why this happened.

Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
  • InvokeRequired is always false for me. – Niels Bosma Dec 07 '14 at 17:17
  • That means that the UIProgress object is created on the wrong thread. Code updated. It is more universal as posted but you'll get a better stack trace when you put the test in your constructor. – Hans Passant Dec 12 '14 at 11:33
3

You are calling UI operation on background thread. This is the reason for that exception. I would use entirely different method to make the progress form the best one is to use Task with IProgress. The other way it to use this:

private void backgroundWorker_ProgressChanged( object sender , ProgressChangedEventArgs e )
    {

      this.UpdateOnMainThread(
        ( ) =>
        {
          wdgProgressBar.Value = e.ProgressPercentage;
          if ( this.Visible == false )
          {
            this.ShowDialog( );
            this.Update( );
          }
        } );
    }

    private void UpdateOnMainThread( Action action )
    {
      if ( this.InvokeRequired )
      {
        this.BeginInvoke( ( MethodInvoker ) action.Invoke);
      }
      else
      {
        action.Invoke( );
      }
    }

    private void backgroundWorker_RunWorkerCompleted( object sender , RunWorkerCompletedEventArgs e )
    {
      this.UpdateOnMainThread(
        ( ) =>
        {
          this.Hide( ); //Here I get a InvalidOperationException
          this.Dispose( );
        } );

    }
insomnium_
  • 1,800
  • 4
  • 23
  • 39
Radin Gospodinov
  • 2,313
  • 13
  • 14
1

Use the BeginInvoke() method on the form:

//http://msdn.microsoft.com/en-us/library/0b1bf3y3(v=vs.110).aspx

    private void backgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        this.BeginInvoke(new InvokeDelegate(InvokeMethod));               
    }

    public delegate void InvokeDelegate();

    public void InvokeMethod()
    {
        this.Hide(); 
        this.Dispose();
    }
opadro
  • 11
  • 3
1

I think you can find some help here: BackgroundWorker hide form window upon completion . However don't forget to detach BackgroundWorker events and stop BackgroundWorker it self like explained in here: Proper way to Dispose of a BackGroundWorker . The problem can be in the

this.Dispose();

in the backgroundWorker_RunWorkerCompleted event. With that you are disposing form page. Is that what you want to do? Or you want to dispose BackgroundWorker? Disposing form page all resources are released so doing this.Hide(); a second time can be a mistake.

For more info, you can see this links: C# Form.Close vs Form.Dispose and Form.Dispose Method

Community
  • 1
  • 1
Ninita
  • 1,209
  • 2
  • 19
  • 45
1

You must check this link How to update the GUI from another thread in C#?

Probably have all the possible answers

Hope this Help

Community
  • 1
  • 1
Miguel
  • 3,786
  • 2
  • 19
  • 32
1

You're running a call to the main thread from a thread that can't manipulate UI. The simplest way is to use anonymous delegate invoke.

Change this:

        if (this.Visible == false)
        {
            this.ShowDialog();
            this.Update();
        }

For this:

this.Invoke((MethodInvoker) delegate { 
        if (this.Visible == false)
        {
            this.ShowDialog();
            this.Update();
        }    
});

It's not the most optimized way but does the job awesomely fast without much recode. :)

Hangarter
  • 582
  • 4
  • 12