0

I am intermittently getting a System.ObjectDisposedException after closing a Modal dialog that displays received BLE adverts. There is a higher chance of the error with increasing advert frequency.

The modal dialog is created by the app main form. In fact I have made it persist for the life of the app, in order to cross off one possible Dispose of an object. Code has been reduced to its most basic form to reproduce the problem and there are now more lines of debug asserts and output than actual code.

Here's the main form. Just one button to show the dialog modally...

using System.Diagnostics;
using TestBle;

namespace AdWatcherCrashTest
{
    public partial class FormMain : Form
    {
        WatcherCrashTest watcherCrashTest;

        public FormMain()
        {
            InitializeComponent();

            Debug.WriteLine("Main Form Constructor: Create Dialog and assign to Main Form field...");
            watcherCrashTest = new();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            Debug.WriteLine("ShowDialog...");
            DialogResult result = watcherCrashTest.ShowDialog(this);
            Debug.Assert(watcherCrashTest != null);
            Debug.WriteLine("ShowDialog returned " + result);
        }
    }
}

And here's the dialog that is displayed when the button is clicked...

using System.Diagnostics;
using Windows.Devices.Bluetooth.Advertisement;

namespace TestBle
{
    public partial class WatcherCrashTest : Form
    {
        private BluetoothLEAdvertisementWatcher deviceWatcher;

        public WatcherCrashTest()
        {
            Debug.WriteLine("Dialog constructor");
            InitializeComponent();

            deviceWatcher = new BluetoothLEAdvertisementWatcher();
        }

        private void WatcherCrashTest_Shown(object sender, EventArgs e)
        {
            Debug.WriteLine("Dialog Shown");
            deviceWatcher.Received += deviceWatcher_Received;
            deviceWatcher.Start();
        }

        private void WatcherCrashTest_FormClosing(object sender, FormClosingEventArgs e)
        {
            Debug.WriteLine("Dialog FormClosing");
            if (deviceWatcher != null)
            {
                deviceWatcher.Stop();
                deviceWatcher.Received -= deviceWatcher_Received;

                // Adding the line below appears to avoid the exception but smells bad to me
                // Application.DoEvents();
            }
        }

        private void deviceWatcher_Received(BluetoothLEAdvertisementWatcher sender, BluetoothLEAdvertisementReceivedEventArgs args)
        {
            String BtDeviceDetails = "0x" + args.BluetoothAddress.ToString("X8") + ": " + args.Advertisement.LocalName;
            Debug.WriteLine(BtDeviceDetails);

            Debug.Assert(deviceWatcher != null);
            Debug.Assert(this != null);
            Debug.Assert(!this.IsDisposed);
            Debug.Assert(!this.Disposing);
            // Debug.Assert(false); // Uncomment to prove out Assert, once losing your mind.

            // Getting intermittent 'Exception User-Unhandled' with whole Invoke statement highlighted.
            // System.ObjectDisposedException: 'Cannot access a disposed object. ObjectDisposed_ObjectName_Name'
            //
            // Invoking with the Owner's method does not cause the exception
            //this.Owner.Invoke(new Action(() =>
            //
            // So 'this' form is supposedly Disposed when the Invoke method is called.
            // But the form is shown with ShowDialog(), so should not be disposed.
            // What the hell???
            //
            this.Invoke(new Action(() =>
            {
                // The same exception is thrown, even if the line below is commented out.
                listBox1.Items.Add(BtDeviceDetails);
            }));
        }
    }
}

The exception is thrown when the dialog is closed using the standard 'cross' close button in the top right corner. Here is the debug output..

Main Form Constructor: Create Dialog and assign to Main Form field...
Dialog constructor
ShowDialog...
Dialog Shown
0x4EA52B5CA482: 
0x78BDBC740D11: 
0x6FE1A3309EF8: 
0x69B9D65D8361: 
0xE135AEE6F3FC: 
0xCD322894FB49: 
0xD0034B541726: 
0x4A47B7552655: 
0x78BDBC740D11: 
0x1A56AD6F4DEF: 
0x63F46825075B: 
0xF71E0F339CFF: 
Dialog FormClosing
ShowDialog returned Cancel
ShowDialog...
Dialog Shown
0xF71E0F339CFF: 
0x6FE1A3309EF8: 
0x4EA52B5CA482: 
0xD0034B541726: 
0x4A47B7552655: 
0xC3B56E06B2D9: 
0x6FE1A3309EF8: 
Dialog FormClosing
Exception thrown: 'System.ObjectDisposedException' in System.Private.CoreLib.dll
An exception of type 'System.ObjectDisposedException' occurred in System.Private.CoreLib.dll but was not handled in user code
Cannot access a disposed object.

In the instance above, the exception was thrown the second time that the form was shown. This varies. Sometimes it is the first. Sometimes it takes several goes.

I have found two ways to prevent the exception, but neither smell right to me...

  1. Add an Application.DoEvents(); in FormClose.
  2. Use the Invoke method of the 'Owner' main form instead of the modal form to populate the listbox in the correct thread.

I don't understand why this (the dialog form) .Invoke causes an exception, since the form is not disposed of by a call to ShowDialog. Clearly the form isn't disposed of, since I can re-open the form by clicking the button on the main form once more. Furthermore, the previous entries in the listbox are still there.

Running a debug build, I get this error. I have yet to get the error on a release build.

Can anyone see why this is happening? Is my coding sense of smell appropriate, or is one of my fixes a fair solution?

I have about 30 years of Delphi experience, but unfortunately more like 30 minutes of c# .net experience (still learning). So it is quite possible that this error is a basic one, though I've spent hours trying to fix it, with plenty of web searches to no avail. Any help or suggestions appreciated.

Steve

  • `Invoke` is culprit ... it is obviously not executed right when it is called ... there is a message queue and Invoke put something on it ... but when there is `WM_CLOSE` already on queue it will be picked first and cause Form.Dispose called ... – Selvin Jan 20 '23 at 14:02
  • @Selvin can you elaborate? Is there something wrong with how I have used Invoke? – Stephen Done Jan 20 '23 at 14:04
  • It may appear because of event handler is executing after form closed. It is good idea to wait when watcher is really stopped after calling Stop() method. And it is good idea to remove event handler before calling Stop() (at least it is what I do in my Bluetooth Framework: I always wait for watcher stopped). – Mike Petrichenko Jan 20 '23 at 14:04
  • add `bool realClosing = false` ... on `FormClosing` check if its `false` call `Watcher.Stop()` , set flag to `true` and cancel closing - if flag is true than allow close ... in `Watcher.Stopped` call `Form.Close()` ... – Selvin Jan 20 '23 at 14:07
  • @MikePetrichenko I swapped the order of the Stop() and event handler removal. I also added while (deviceWatcher.Status != BluetoothLEAdvertisementWatcherStatus.Stopped) { Application.DoEvents(); } after deviceWatcher.Stop(); and I still get the error. Is this what you meant? – Stephen Done Jan 20 '23 at 14:17
  • Try to move your code from FormClosing to FormClosed. Also set deviceWatcher to null after stopping it. – Mike Petrichenko Jan 20 '23 at 14:27
  • @Selvin, I understand your suggestion (good idea!) and tried it. The intermittent exception then moves to the Invoke in the Stopped event. – Stephen Done Jan 20 '23 at 14:30
  • @MikePetrichenko, exception the same in closed or closing. Also note I am NOT disposing of the deviceWatcher, as the form persists between ShowDialog events - I don't dispose of the form - it's held in a field of the main form and created once in the main form constructor. I don't understand what is being described as disposed in the exception, as the form is not disposed. – Stephen Done Jan 20 '23 at 14:36
  • What about calling `deviceWatcher.Received -= deviceWatcher_Received;` before `Stop` ... Why it may help? Well , maybe event is fired when `Stop` is already executing ... problem is that we don't know what is disposed ... – Selvin Jan 20 '23 at 14:36
  • @Selvin, yes Mike suggested that. I tried it just now and still the same result. – Stephen Done Jan 20 '23 at 14:37
  • it may be `listBox1.Items.Add(BtDeviceDetails);` ... So also worth of trying is check if Form is disposed inside Invoke delegate – Selvin Jan 20 '23 at 14:39
  • @Selvin, I still get the exception, even if I comment out all code inside the Invoke! It is the Invoke call that is a problem, not the contents, but I don't understand why. – Stephen Done Jan 20 '23 at 14:40
  • hehe ... ok, f* it ... check the `realClosing` (make it `volatile`) from the `deviceWatcher_Received` and skip all code if `realClosing` is true – Selvin Jan 20 '23 at 14:44
  • Try to switch to early .NET version. May be something in .NET itself. All the code looks absolutely OK. However I saw too many bugs in MS libs/.NET/APIs to be sure my/your code is correct and to recomend to try other .NET version. – Mike Petrichenko Jan 20 '23 at 14:45
  • @MikePetrichenko I tried 6.0 instead of 7.0 and same problem! – Stephen Done Jan 20 '23 at 14:53
  • Send me your test project (mike@btframework.com). Problem looks very interesting and I do really want to find what is going wrong. – Mike Petrichenko Jan 20 '23 at 14:57
  • Thanks @MikePetrichenko. Will do. I don't like using fixes that smell bad. And maybe we can learn something from the problem. School run first, then I'll mail it across! – Stephen Done Jan 20 '23 at 15:01
  • @MikePetrichenko found this thread which is quite closely related... [link](https://stackoverflow.com/questions/12956288/avoiding-objectdisposedexception-while-calling-invoke). I've not been able to fix my problem yet, but this link is very interesting. – Stephen Done Jan 20 '23 at 22:33

1 Answers1

0

@MikePetrichenko was kind enough to produce the following code, with which I have been unable to recreate the exception. This is very similar to the solution suggest by @Selvin in the comments. Thank you both for your help.

using System.Diagnostics;
using Windows.Devices.Bluetooth.Advertisement;

namespace TestBle
{
    public partial class WatcherCrashTest : Form
    {
        private BluetoothLEAdvertisementWatcher deviceWatcher;
        private Boolean CanClose;
        
        public WatcherCrashTest()
        {
            InitializeComponent();

            deviceWatcher = new BluetoothLEAdvertisementWatcher();
            deviceWatcher.Stopped += DeviceWatcher_Stopped;
        }

        private void WatcherCrashTest_Shown(object sender, EventArgs e)
        {
            CanClose = false;

            deviceWatcher.Received += deviceWatcher_Received;
            deviceWatcher.Start();
        }

        private void DeviceWatcher_Stopped(BluetoothLEAdvertisementWatcher sender, BluetoothLEAdvertisementWatcherStoppedEventArgs args)
        {
            CanClose = true;
            this.Invoke(new Action(() =>
            {
                this.Close();
            }));
        }

        private void WatcherCrashTest_FormClosing(object sender, FormClosingEventArgs e)
        {
            if (deviceWatcher.Status == BluetoothLEAdvertisementWatcherStatus.Started)
            {
                deviceWatcher.Received -= deviceWatcher_Received;
                deviceWatcher.Stop();
            }
            e.Cancel = !CanClose;
        }

        private void deviceWatcher_Received(BluetoothLEAdvertisementWatcher sender, BluetoothLEAdvertisementReceivedEventArgs args)
        {
            String BtDeviceDetails = "0x" + args.BluetoothAddress.ToString("X8") + ": " + args.Advertisement.LocalName;
            this.Invoke(new Action(() =>
            {
                listBox1.Items.Add(BtDeviceDetails);
            }));
        }
    }
}

This SO post highlighted by Mike gave me some more ideas... Avoiding `ObjectDisposedException` while calling `Invoke`

Analysis of the problem:

I did some further research and debugging and found the source of the exception to be within the Invoke call itself. Invoke() posts a message from the non-UI thread back to the UI thread, where it executes the specified Action using the UI thread. So the invoke calls end up in the message queue for the UI thread. No amount of asserts or conditions prior to the Invoke() call can prevent the possibility of an exception. This is because at the time the checks are done, they are all passed and the Invoke() call posts a message to the UI thread. The exception occurs in the situation where the form closed occurs and the message queue is processed afterwards. The message posted by Invoke() is then processed and it is unable to run the code. As a result it raises the exception.

Possible solutions:

  • As I had noted in my original question, an Application.DoEvents() in the FormClose event fixes things. I now believe this is because it ensures all the posted Invoke() messages are processed before the form closes.
  • Mike's solution works because, after stopping the ad watcher, an Invoke() call is used to finally close the form. Because the message queue has to be processed to run the Form.Close() event within that Invoke(), the other Invoke() calls in the message queue will also get processed. So I believe this removes the possibility of further Invoke() calls being processed after the FormClose, which would cause these exceptions.

I think that these general conclusions would apply to most situations where there are a stream of Invoke calls and a closing form causing exceptions. I.e. There is nothing special about me using a BLE Ad Watcher - the same errors could just as well be caused by any code in another thread using Invoke to access the UI.