5

I have 2 forms, one is MainForm and second is DebugForm. The MainForm has a button that sets up and shows the DebugForm like this, And passes a reference to an already opened SerialPort:

private DebugForm DebugForm; //Field
private void menuToolsDebugger_Click(object sender, EventArgs e)
{
    if (DebugForm != null)
    {
        DebugForm.BringToFront();
        return;
    }

    DebugForm = new DebugForm(Connection);

    DebugForm.Closed += delegate
    {
        WindowState = FormWindowState.Normal;
        DebugForm = null;
    };

    DebugForm.Show();
}

In the DebugForm, I append a method to handle the DataReceived event of the serialport connection (in DebugForm's constructor):

public DebugForm(SerialPort connection)
{
    InitializeComponent();
    Connection = connection;
    Connection.DataReceived += Connection_DataReceived;
}

Then in the Connection_DataReceived method, I update a TextBox in the DebugForm, that is using Invoke to do the update:

private void Connection_DataReceived(object sender, SerialDataReceivedEventArgs e)
{           
    _buffer = Connection.ReadExisting();
    Invoke(new EventHandler(AddReceivedPacketToTextBox));
}

But I have a problem. As soon as I close the DebugForm, it throws an ObjectDisposedException on the Invoke(new EventHandler(AddReceivedPacketToTextBox)); Line.

How can I fix this? Any tips/helps are welcome!

UPDATE

I found out if I remove the event in a button event click , and close the form in that button click, everything is fine and my debugform gets closed without any exception...how odd!

private void button1_Click(object sender, EventArgs e)
{
    Connection.DataReceived -= Connection_DebugDataReceived;
    this.Close();
}
Dumbo
  • 13,555
  • 54
  • 184
  • 288
  • Well if you can't detach in the Dispose() method (WHY?!) you may do it in the OnClosed() method (it's sure it'll be called) and add a check in Connection_DataReceived to call Invoke() only if IsDisposed is false. – Adriano Repetti Oct 18 '12 at 14:13
  • I put that evnet detach in `Dispose()` method but I had same problem. I even tried to put a `if(Dispoing == false)` before the `Invoke` also didn't help. – Dumbo Oct 18 '12 at 14:14
  • not **Disposing** but **IsDisposed** – Adriano Repetti Oct 18 '12 at 14:17
  • Well, it may be closing _during the Invoke()_, I don't know how framework will handle this. Did you try with BeginInvoke instead? Does it works if you detach from OnClosed()? – Adriano Repetti Oct 18 '12 at 14:24
  • I do not like how you are handling Debug.Closed in the MainForm. You should override OnClosed and disconnect from the event there. Here's why, your DebugForm connects to the event, therefore, it should also disconnect from the event (who's responsible for this?) – Alan Oct 18 '12 at 16:29

3 Answers3

6

Closing a form disposes of the Form object but cannot forcibly remove references that other classes have to it. When you register your form for events, you are basically giving a reference to your form object to the source of the events (the SerialPort instance in this case).

This means that, even though your form is closed, the event source (your SerialPort object) is still sending events to the form instance and the code to handle these events is still being run. The problem then is that when this code tries to update the disposed form (set its title, update its controls, call Invoke, &c.) you will get this exception.

So what you need to do is ensure that the event gets deregistered when your form closes. This is as simple as detecting that the form is closing and unregister the Connection_DataReceived event handler. Handily you can detect the form is closing by overriding the OnFormClosing method and unregistering the event in there:

protected override OnFormClosing(FormClosingEventArgs args)
{
    Connection.DataReceived -= Connection_DataReceived;
}

I would also recommend moving the event registration to an override of the OnLoad method as otherwise it may receive events before the form has been fully constructed which could cause confusing exceptions.

Paul Ruane
  • 37,459
  • 12
  • 63
  • 82
  • I tried that, still same problem...maybe it has something to do with `DebugForm = null` in the form closed event? – Dumbo Oct 18 '12 at 14:35
  • 1
    @Saeid87 well, possible even if you detach inside the **OnClosed() of DebugForm (before the call to base)** it should work well... – Adriano Repetti Oct 18 '12 at 15:48
  • @Saeid87 I guess you did try everything (override Dispose() and OnClosed() inside DebugForm, not its container. – Adriano Repetti Oct 18 '12 at 15:49
  • This looks correct to me, it might be possible that an Invoke is getting queued, however,before your OnClosing finishes.. In this case you could perhaps Try{}catch{} around the Invoke and ignore the error (Just make sure that it isn't getting hit repeatedly). It is probably better to try if(!IsDisposed) Invoke... but again this would only to prevent a one-off race condition, do not let it stay wired and continue firing these events. – Alan Oct 18 '12 at 16:34
  • Well I could use the try catch block in first place but really what if the exception gets called for other reasons rather than cross thread problems?! – Dumbo Oct 18 '12 at 19:05
  • @Saeid87 - If it's due to queued events, call Application.DoEvents() after de-registering the event. – H H Oct 18 '12 at 20:44
  • @HenkHolterman - "f it's due to queued events, call Application.DoEvents() after de-registering the event. " - that's not enough. There is still a race condition whereby the form can be closed *after* the DataReceived event has been fired, but *before* the handler has called Invoke to queue the event. – Joe Oct 20 '12 at 16:27
2

You haven't shown the code for the AddReceivedPacketToTextBox method.

You could try checking for a disposed form in that method:

private void AddReceivedPacketToTextBox(object sender, EventArgs e)
{
    if (this.IsDisposed) return;

    ...
}

Detaching the DataReceived event handler when closing the form is probably a good idea, but isn't sufficient: there is still a race condition which means your AddReceivedPacketToTextBox can be called after the form is closed/disposed. The sequence would be something like:

  • Worker thread: DataReceived event fired, Connection_DataReceived starts executing
  • UI thread: Form closed and disposed, DataReceived event detached.
  • Worker thread: calls Invoke
  • UI thread: AddReceivedPacketToTextBox executed while form is disposed.

I found out if I remove the event in a button event click , and close the form in that button click, everything is fine and my debugform gets closed without any exception...how odd!

That's not odd. Multithreading bugs ("Heisenbugs") are timing-related and small changes like that can affect the timing. But it's not a robust solution.

Joe
  • 122,218
  • 32
  • 205
  • 338
  • Thanks I will try your code too if it works then I can get rid of timer stuff. – Dumbo Oct 20 '12 at 14:54
  • A little late to the game, but testing for disposed here would not work... the exception is thrown *before* `AddReceivedPacketToTextBox()` is called. – tronman Aug 18 '17 at 15:45
0

The problem could be solved by adding a timer:

  bool formClosing = false;
    private void Connection_DataReceived(object sender, SerialDataReceivedEventArgs e)
    {
      if (formClosing) return;
      _buffer = Connection.ReadExisting();
      Invoke(new EventHandler(AddReceivedPacketToTextBox));
    }
    protected override void OnFormClosing(FormClosingEventArgs e)
    {
      base.OnFormClosing(e);
      if (formClosing) return;
      e.Cancel = true;
      Timer tmr = new Timer();
      tmr.Tick += Tmr_Tick;
      tmr.Start();
      formClosing = true;
    }
    void Tmr_Tick(object sender, EventArgs e)
    {
      ((Timer)sender).Stop();
      this.Close();
    }

Thanks to JohnWein from MSDN

Dumbo
  • 13,555
  • 54
  • 184
  • 288
  • This seems like overkill. I'm pretty sure adding a test for Form.IsDisposed in the AddReceivedPacketToTextBox method will do the job without any timers. – Joe Oct 20 '12 at 19:15