Как избежать `ObjectDisposedException` при вызове `Invoke`

У меня есть 2 формы, одна MainForm, а вторая DebugForm. В MainForm есть кнопка, которая настраивает и отображает DebugForm следующим образом, и передает ссылку на уже открытый 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();
}

В DebugForm я добавляю метод для обработки события DataReceived соединения через последовательный порт (в конструкторе DebugForm):

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

Затем в методе Connection_DataReceived я обновляю TextBox в DebugForm, используя Invoke для обновления:

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

Но у меня есть проблема. Как только я закрываю DebugForm, он выдает ObjectDisposedException в строке Invoke(new EventHandler(AddReceivedPacketToTextBox));.

Как я могу это исправить? Любые советы/помощь приветствуются!

ОБНОВЛЕНИЕ

Я узнал, что если я удаляю событие при нажатии кнопки и закрываю форму при нажатии этой кнопки, все в порядке, и моя отладочная форма закрывается без каких-либо исключений... как странно!

private void button1_Click(object sender, EventArgs e)
{
    Connection.DataReceived -= Connection_DebugDataReceived;
    this.Close();
}

person Saeid Yazdani    schedule 18.10.2012    source источник
comment
Ну, если вы не можете отсоединиться в методе Dispose() (ПОЧЕМУ?!), вы можете сделать это в методе OnClosed() (он обязательно будет вызван) и добавить проверку в Connection_DataReceived для вызова Invoke(), только если IsDisposed ложно.   -  person Adriano Repetti    schedule 18.10.2012
comment
Я поместил этот evnet detach в метод Dispose(), но у меня была такая же проблема. Я даже пытался поставить if(Dispoing == false) перед Invoke тоже не помогло.   -  person Saeid Yazdani    schedule 18.10.2012
comment
не Удаление, а Утилизация   -  person Adriano Repetti    schedule 18.10.2012
comment
Ну, это может быть закрытие во время Invoke(), я не знаю, как фреймворк справится с этим. Вы пытались вместо этого использовать BeginInvoke? Работает ли это, если вы отсоединитесь от OnClosed()?   -  person Adriano Repetti    schedule 18.10.2012
comment
Мне не нравится, как вы обрабатываете Debug.Closed в MainForm. Вы должны переопределить OnClosed и отключиться от события. Вот почему ваша DebugForm подключается к событию, следовательно, она также должна отключаться от события (кто за это отвечает?)   -  person Alan    schedule 18.10.2012


Ответы (3)


Закрытие формы удаляет объект Form, но не может принудительно удалить ссылки, которые есть на него у других классов. Когда вы регистрируете свою форму для событий, вы в основном даете ссылку на свой объект формы источнику событий (в данном случае экземпляр SerialPort).

Это означает, что даже если ваша форма закрыта, источник событий (ваш объект SerialPort) по-прежнему отправляет события в экземпляр формы, и код для обработки этих событий все еще выполняется. Проблема заключается в том, что когда этот код пытается обновить удаленную форму (установить ее заголовок, обновить ее элементы управления, вызвать Invoke и т. д.), вы получите это исключение.

Итак, что вам нужно сделать, это убедиться, что событие будет отменено при закрытии вашей формы. Это так же просто, как определить, что форма закрывается, и отменить регистрацию обработчика событий Connection_DataReceived. Вы можете легко определить, что форма закрывается, переопределив метод OnFormClosing и отменив регистрацию события:

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

Я бы также рекомендовал переместить событие registration в переопределение метода OnLoad, поскольку в противном случае он может получать события до того, как форма будет полностью построена, что может привести к запутанным исключениям.

person Paul Ruane    schedule 18.10.2012
comment
Я пробовал это, все та же проблема ... может быть, это как-то связано с DebugForm = null в закрытом событии формы? - person Saeid Yazdani; 18.10.2012
comment
@Saeid87 Saeid87 ну, возможно, даже если вы отсоединитесь внутри OnClosed() от DebugForm (перед вызовом базы), это должно работать хорошо... - person Adriano Repetti; 18.10.2012
comment
@Saeid87 Saeid87 Думаю, вы все перепробовали (переопределите Dispose () и OnClosed () внутри DebugForm, а не в его контейнере. - person Adriano Repetti; 18.10.2012
comment
Это выглядит правильно для меня, возможно, что Invoke ставится в очередь до того, как ваше OnClosing завершится. не получает повторных ударов). Вероятно, лучше попробовать if(!IsDisposed) Invoke... но опять же, это только для предотвращения одноразового состояния гонки, не позволяйте ему оставаться проводным и продолжать запускать эти события. - person Alan; 18.10.2012
comment
Ну, я мог бы использовать блок try catch в первую очередь, но что, если исключение вызывается по другим причинам, а не из-за проблем с перекрестными потоками?! - person Saeid Yazdani; 18.10.2012
comment
@ Saeid87. Если это связано с событиями в очереди, вызовите Application.DoEvents() после отмены регистрации события. - person Henk Holterman; 19.10.2012
comment
@HenkHolterman - если это связано с событиями в очереди, вызовите Application.DoEvents() после отмены регистрации события. - этого не достаточно. По-прежнему существует состояние гонки, при котором форма может быть закрыта после запуска события DataReceived, но до вызова обработчиком Invoke для постановки события в очередь. - person Joe; 20.10.2012

Вы не показали код метода AddReceivedPacketToTextBox.

Вы можете попробовать проверить удаленную форму в этом методе:

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

    ...
}

Отключение обработчика события DataReceived при закрытии формы, вероятно, хорошая идея, но этого недостаточно: все еще существует условие гонки, которое означает, что ваш AddReceivedPacketToTextBox может быть вызван после закрытия/удаления формы. Последовательность будет примерно такой:

  • Рабочий поток: событие DataReceived запущено, Connection_DataReceived начинает выполняться
  • Поток пользовательского интерфейса: форма закрыта и удалена, событие DataReceived отсоединено.
  • Рабочий поток: вызывает Invoke
  • Поток пользовательского интерфейса: AddReceivedPacketToTextBox выполняется при удалении формы.

Я узнал, что если я удаляю событие при нажатии кнопки и закрываю форму при нажатии этой кнопки, все в порядке, и моя отладочная форма закрывается без каких-либо исключений... как странно!

Это не странно. Ошибки многопоточности («Heisenbugs») связаны со временем, и небольшие изменения, подобные этому, могут повлиять на время. Но это не надежное решение.

person Joe    schedule 20.10.2012
comment
Спасибо, я тоже попробую ваш код, если он сработает, тогда я смогу избавиться от таймера. - person Saeid Yazdani; 20.10.2012
comment
Немного поздно для игры, но тестирование для disposed здесь не сработает... исключение выдается до вызова AddReceivedPacketToTextBox(). - person tronman; 18.08.2017

Проблема может быть решена путем добавления таймера:

  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();
    }

Спасибо JohnWein из MSDN

person Saeid Yazdani    schedule 18.10.2012
comment
Это кажется излишеством. Я почти уверен, что добавление теста для Form.IsDisposed в методе AddReceivedPacketToTextBox сделает работу без каких-либо таймеров. - person Joe; 20.10.2012