BackgroundWorker: InvalidOperationException in RunWorkerCompleted
Asked Answered
C

6

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();
Complaisant answered 7/12, 2014 at 15:12 Comment(4)
How Start is invoked?Thickskinned
I have found an old post 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.Thickskinned
Same as kennyzx.. here is another post - blogs.msdn.com/b/kaelr/archive/2007/09/05/… I guess this has something to do with how Excel-DNA thing works (never used or even heard of it before.. but looks cool)Northumbria
The best option is to use the begininvoke in the callbackCenotaph
S
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.

Shushan answered 7/12, 2014 at 15:39 Comment(2)
InvokeRequired is always false for me.Complaisant
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.Shushan
B
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( );
        } );

    }
Bummer answered 15/12, 2014 at 10:58 Comment(0)
B
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();
    }
Beadsman answered 11/12, 2014 at 3:0 Comment(0)
I
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

Illuse answered 16/12, 2014 at 9:43 Comment(0)
F
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

Firehouse answered 16/12, 2014 at 15:18 Comment(0)
U
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. :)

Undermine answered 16/12, 2014 at 16:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.