1

Good day!

I have custom popupcontainer control - comboBox where you can put other controls and they will appear on pop up. In my case it contains datagrid. I want to make it work fast - I need that datagrid only when user decides to pop up my control. I decided to move datagrid creation to other thread and use Backgroundworker for it, since it perfectly suits for UI and I don't want to mess with Control.Invoke. Here is my simplified code:

    protected override void  OnCreateControl()
    {
        base.OnCreateControl();
        CreateContainer();
    }
    protected virtual void CreateContainer()
    {
        _worker = new BackgroundWorker();
        _worker.DoWork += DoActionsOnAsyncWork;
        _worker.RunWorkerCompleted += AsyncWorker_RunWorkerCompleted;
        _worker.RunWorkerAsync(this);
    }
    protected void DoActionsOnAsyncWork(object sender, DoWorkEventArgs e)
    {
        var _grid = ///Create and initialize local grid here
        e.Result = _grid; // Throw it to RunWorkerCompleted event
    }
    private void AsyncWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        if (!e.Cancelled && e.Error == null && !IsDisposed && !IsDisposing)
        {
            if (!_closing)
            {
              this.Grid = e.Result as MyGrid;
              AttachEventHandlersToGrid();
            } else
              (e.Result as MyGrid).Dispose(); // Get rid of grid
        }

        // Get rid of worker
        _worker.DoWork -= DoActionsOnAsyncWork;
        _worker.RunWorkerCompleted -= AsyncWorker_RunWorkerCompleted;
        _worker.Dispose();
        _worker = null;
    }
    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            WaitForBackgroundWorker();
            // Dispose other stuff
            ...
        }
        base.Dispose(disposing);
    }

    protected void WaitForBackgroundWorker()
    {
        if (_worker == null ||
            !_worker.IsBusy)
            return;
        _closing = true;

        while (_worker != null && _worker.IsBusy)
            Application.DoEvents(); // Hack throw worker.RunWorkerCompleted on top of msg queue
    }

Now, I need to wait _worker in order to properly dispose my control(form is greater scope). Furthermore _worker creates grid that is also needs to be disposed. My question is - how can I wait for backgroundworker without Application.DoEvents(). Because sometimes(often on remote desktop - maybe some other painting algorithms?) it causes the whole application to hang. Call stack:

 mscorlib.dll!System.Threading.WaitHandle.InternalWaitOne(System.Runtime.InteropServices.SafeHandle waitableSafeHandle, long millisecondsTimeout, bool hasThreadAffinity, bool exitContext) + 0x1f bytes     
 mscorlib.dll!System.Threading.WaitHandle.WaitOne(long timeout, bool exitContext) + 0x23 bytes   
 mscorlib.dll!System.Threading.WaitHandle.WaitOne(int millisecondsTimeout, bool exitContext) + 0x1c bytes    
 System.Windows.Forms.dll!System.Windows.Forms.Control.WaitForWaitHandle(System.Threading.WaitHandle waitHandle) + 0x96 bytes    
 System.Windows.Forms.dll!System.Windows.Forms.Control.MarshaledInvoke(System.Windows.Forms.Control caller, System.Delegate method, object[] args, bool synchronous) + 0x34b bytes   
 System.Windows.Forms.dll!System.Windows.Forms.Control.Invoke(System.Delegate method, object[] args) + 0x50 bytes    
 System.Windows.Forms.dll!System.Windows.Forms.WindowsFormsSynchronizationContext.Send(System.Threading.SendOrPostCallback d, object state) + 0x56 bytes     
 System.dll!Microsoft.Win32.SystemEvents.SystemEventInvokeInfo.Invoke(bool checkFinalization, object[] args) + 0x66 bytes    
 System.dll!Microsoft.Win32.SystemEvents.RaiseEvent(bool checkFinalization, object key, object[] args) + 0x110 bytes     
 System.dll!Microsoft.Win32.SystemEvents.RaiseEvent(object key, object[] args) + 0xe bytes   
 System.dll!Microsoft.Win32.SystemEvents.OnUserPreferenceChanged(int msg, System.IntPtr wParam, System.IntPtr lParam) + 0x76 bytes   
 System.dll!Microsoft.Win32.SystemEvents.WindowProc(System.IntPtr hWnd, int msg, System.IntPtr wParam, System.IntPtr lParam) + 0x2c6 bytes   
 [Native to Managed Transition]  
 [Managed to Native Transition]  
 System.Windows.Forms.dll!System.Windows.Forms.Application.ComponentManager.System.Windows.Forms.UnsafeNativeMethods.IMsoComponentManager.FPushMessageLoop(System.IntPtr dwComponentID, int reason, int pvLoopData) + 0x357 bytes    
 System.Windows.Forms.dll!System.Windows.Forms.Application.ThreadContext.RunMessageLoopInner(int reason, System.Windows.Forms.ApplicationContext context) + 0x33d bytes  
 System.Windows.Forms.dll!System.Windows.Forms.Application.ThreadContext.RunMessageLoop(int reason, System.Windows.Forms.ApplicationContext context) + 0x5f bytes    
 System.Windows.Forms.dll!System.Windows.Forms.Application.DoEvents() + 0x18 bytes   
 MyControl.WaitForBackgroundWorker() Line 874 + 0x1b bytes        
 ...  // Many recursive disposes    
 Form.Dispose()
nikita
  • 2,737
  • 2
  • 20
  • 26
  • 1
    You cannot wait, that will cause deadlock. Check this answer: http://stackoverflow.com/a/1732361/17034 – Hans Passant Nov 27 '12 at 20:46
  • @Hans Passant Then I'm puzzled. Is there really no way to wait for `backgroundworker` to complete its two events - `DoWork` and `RunWorkerCompleted`? I have `QueryPopUp` event where I use Application.DoEvents() without loop and it works(lucky?). I've tested it by Thread.Sleep() in `backgroundworker.DoWork` event. Another words may one call Application.DoEvents() always force backgroundworker.RunWorkerCompleted to complete? – nikita Nov 27 '12 at 21:55
  • RunWorkerCompleted is the problem, it can only run when the UI thread is pumping messages. Yes, DoEvents is a hack to avoid the deadlock. Dragons live there. – Hans Passant Nov 27 '12 at 23:08

1 Answers1

1

Because of this line:

while (_worker != null && _worker.IsBusy)
            Application.DoEvents(); // Hack throw worker.RunWorkerCompleted on top of msg queue

Even if the DoWork has finished, your while loop will prevent RunWorkerCompleted to get executed because they are in the same thread (while loop keeps executing.

Change the code like this:

private void AsyncWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            if (!e.Cancelled && e.Error == null && !IsDisposed && !IsDisposing)
            {
                if (!_closing)
                {
                    this.Grid = e.Result as MyGrid;
                    AttachEventHandlersToGrid();
                }
                else
                {
                    (e.Result as MyGrid).Dispose(); // Get rid of grid
                }
            }

            // Get rid of worker
            _worker.DoWork -= DoActionsOnAsyncWork;
            _worker.RunWorkerCompleted -= AsyncWorker_RunWorkerCompleted;
            _worker.Dispose();
            _worker = null;
            if(_closing)
                this.Dispose(); //call Dispose again, now you know that worker has finished
        }
        protected bool HasBackgroundWorkerFinished()
        {
            if (_worker == null ||
                !_worker.IsBusy)
                return true;
            _worker.CancelAsync();
            _closing = true; //this means that worker is busy
            return false;
        }
        protected override void Dispose(bool disposing)
        {
            bool dispose = true;
            if (disposing)
            {
                dispose = HasBackgroundWorkerFinished();
                if (dispose)
                {
                    // Dispose other stuff
                }
            }
            if(dispose) //don't dispose if worker didn't finish
                base.Dispose(disposing);
        }

Set the _worker to accept canceling and add this code multiple times

if (_worker.CancellationPending)
{
        e.Cancel = true;
        return;
}

to your DoWork code between all major code parts

Nikola Davidovic
  • 8,556
  • 1
  • 27
  • 33
  • It is a good solution, since I don't mess with form closing event, but it prevents GC from collecting control => form on form.Close() event. Wouldn't GC mark form object somehow because it failed to dispose it? Would it be disposed on next GC.Collect? – nikita Nov 27 '12 at 21:45
  • Dispose will be called again but after worker finishes. check the end of the DoWork event. – Nikola Davidovic Nov 27 '12 at 21:53