【问题标题】:Avoiding `ObjectDisposedException` while calling `Invoke`在调用`Invoke`时避免`ObjectDisposedException`
【发布时间】:2012-10-18 13:50:49
【问题描述】:

我有两种形式,一种是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方法中,我在DebugForm中更新了一个TextBox,也就是使用Invoke进行更新:

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

但我有一个问题。一旦我关闭 DebugForm,它就会在 Invoke(new EventHandler(AddReceivedPacketToTextBox)); 行上抛出一个 ObjectDisposedException

我该如何解决这个问题?欢迎任何提示/帮助!

更新

我发现如果我在按钮事件 click 中删除事件,并在该按钮单击中关闭表单,一切都很好,我的调试表单毫无例外地被关闭......多么奇怪!

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

【问题讨论】:

  • 好吧,如果你不能在 Dispose() 方法中分离(为什么?!)你可以在 OnClosed() 方法中进行(它肯定会被调用)并添加一个签入Connection_DataReceived 仅在 IsDisposed 为 false 时调用 Invoke()。
  • 我将 evnet detach 放在 Dispose() 方法中,但我遇到了同样的问题。我什至尝试在Invoke 之前添加if(Dispoing == false) 也没有帮助。
  • 不是 Disposed 而是 IsDisposed
  • 嗯,它可能会在 Invoke() 期间关闭 ,我不知道框架将如何处理这个问题。您是否尝试过使用 BeginInvoke?如果你从 OnClosed() 中分离它是否有效?
  • 我不喜欢您在 MainForm 中处理 Debug.Closed 的方式。您应该覆盖 OnClosed 并与那里的事件断开连接。这就是为什么,您的 DebugForm 连接到事件,因此,它也应该与事件断开连接(谁对此负责?)

标签: c# .net winforms multithreading invoke


【解决方案1】:

关闭表单会释放表单对象,但不能强制删除其他类对其的引用。当您为事件注册表单时,您基本上是将表单对象的引用提供给事件源(在本例中为 SerialPort 实例)。

这意味着,即使您的表单已关闭,事件源(您的SerialPort 对象)仍在向表单实例发送事件,并且处理这些事件的代码仍在运行。那么问题是,当这段代码尝试更新已处置的表单(设置其标题、更新其控件、调用Invoke 等)时,您将收到此异常。

因此,您需要做的是确保在表单关闭时取消注册该事件。这就像检测表单正在关闭并取消注册Connection_DataReceived 事件处理程序一样简单。您可以通过覆盖 OnFormClosing 方法并在其中取消注册事件来方便地检测到表单正在关闭:

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

我还建议将事件 registration 移动到 OnLoad 方法的覆盖,否则它可能会在表单完全构造之前接收事件,这可能会导致混乱的异常。

【讨论】:

  • 我试过了,还是一样的问题...可能和DebugForm = null表单关闭事件有关?
  • @Saeid87 好吧,即使您在 DebugForm 的 OnClosed() 内部分离(在调用 base 之前) 也应该可以正常工作...
  • @Saeid87 我猜你确实尝试了一切(在 DebugForm 中覆盖 Dispose() 和 OnClosed(),而不是它的容器。
  • 这对我来说看起来是正确的,但是,在 OnClosing 完成之前,Invoke 可能正在排队。在这种情况下,您也许可以尝试{}catch{} 绕过 Invoke 并忽略错误(只要确保它没有被反复击中)。尝试 if(!IsDisposed) Invoke 可能会更好......但这只是为了防止一次性竞争条件,不要让它保持连线并继续触发这些事件。
  • 好吧,我可以首先使用 try catch 块,但如果异常因为其他原因而不是跨线程问题而被调用怎么办?!
【解决方案2】:

您还没有显示AddReceivedPacketToTextBox 方法的代码。

您可以尝试在该方法中检查已处理的表单:

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

    ...
}

在关闭表单时分离DataReceived 事件处理程序可能是一个好主意,但这还不够:仍然存在竞争条件,这意味着您的AddReceivedPacketToTextBox 可以在表单关闭/处理后调用。顺序如下:

  • 工作线程:触发 DataReceived 事件,Connection_DataReceived 开始执行
  • UI 线程:表单关闭并释放,DataReceived 事件分离。
  • 工作线程:调用 Invoke
  • UI 线程:在处理表单时执行 AddReceivedPacketToTextBox。

我发现如果我在按钮事件 click 中删除事件,并在该按钮单击中关闭表单,一切都很好,我的调试表单毫无例外地被关闭......多么奇怪!

这并不奇怪。多线程错误(“Heisenbugs”)是与时间相关的,像这样的小变化会影响时间。但这不是一个强大的解决方案。

【讨论】:

  • 谢谢,如果它有效,我也会尝试你的代码,然后我可以摆脱计时器的东西。
  • 游戏有点晚了,但在这里对disposed 的测试不起作用...抛出异常之前 AddReceivedPacketToTextBox() 被调用。
【解决方案3】:

这个问题可以通过添加一个定时器来解决:

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

感谢MSDN的JohnWein

【讨论】:

  • 这似乎有点矫枉过正。我很确定在 AddReceivedPacketToTextBox 方法中添加对 Form.IsDisposed 的测试将在没有任何计时器的情况下完成这项工作。
猜你喜欢
  • 2010-12-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-06-22
  • 1970-01-01
  • 2011-01-22
  • 2011-08-26
相关资源
最近更新 更多