在处理控件时避免调用Invoke

时间:2009-12-09 15:39:05

标签: c# controls dispose invoke

我的工作线程中有以下代码(下面的ImageListView来自Control):

if (mImageListView != null && 
    mImageListView.IsHandleCreated &&
    !mImageListView.IsDisposed)
{
    if (mImageListView.InvokeRequired)
        mImageListView.Invoke(
            new RefreshDelegateInternal(mImageListView.RefreshInternal));
    else
        mImageListView.RefreshInternal();
}

但是,我有时会使用ObjectDisposedException方法获得Invoke。看来控件可以在我检查IsDisposed和拨打Invoke之间处理。我怎么能避免这种情况?

14 个答案:

答案 0 :(得分:19)

这里有race condition。你最好只是捕获ObjectDisposed异常并完成它。事实上,我认为在这种情况下它是唯一的工作解决方案。

try
{
    if (mImageListView.InvokeRequired)
       mImageListView.Invoke(new YourDelegate(thisMethod));
    else
       mImageListView.RefreshInternal();
} 
catch (ObjectDisposedException ex)
{
    // Do something clever
}

答案 1 :(得分:14)

您的代码中存在隐含的竞争条件。控件可以在IsDisposed测试和InvokeRequired测试之间进行处理。 InvokeRequired和Invoke()之间还有另一个。如果不确保控件的使用寿命超过线程的寿命,则无法解决此问题。鉴于您的线程正在为列表视图生成数据,它应该在列表视图消失之前停止运行。

通过在FormClosing事件中设置e.Cancel并通过ManualResetEvent指示线程停止来执行此操作。线程完成后,再次调用Form.Close()。使用BackgroundWorker可以轻松实现线程完成逻辑,在this post中找到示例代码。

答案 2 :(得分:3)

尝试使用

if(!myControl.Disposing)
    ; // invoke here

我和你有完全相同的问题。自从我切换到检查.Disposing控件后,ObjectDisposedException就消失了。不是说这会在100%的时间里修复它,只有99%;)在检查到Disposing和调用调用之间仍然存在竞争条件的可能性,但在测试中我已经完成了我没有运行进入它(我使用ThreadPool和工作线程)。

这是我在每次调用调用之前使用的内容:

    private bool IsControlValid(Control myControl)
    {
        if (myControl == null) return false;
        if (myControl.IsDisposed) return false;
        if (myControl.Disposing) return false;
        if (!myControl.IsHandleCreated) return false;
        if (AbortThread) return false; // the signal to the thread to stop processing
        return true;
    }

答案 3 :(得分:3)

现实情况是,使用Invoke和朋友,您无法完全防止对已处置组件的调用,或者由于缺少句柄而导致InvalidOperationException。我还没有真正看到一个答案,比如下面的一个答案,在解决真正基本问题的任何线程中,这些问题都不能通过抢占式测试或使用锁语义完全解决。

这是正常的'正确'成语:

// the event handler. in this case preped for cross thread calls  
void OnEventMyUpdate(object sender, MyUpdateEventArgs e)
{
    if (!this.IsHandleCreated) return;  // ignore events if we arn't ready, and for
                                        // invoke if cant listen to msg queue anyway
    if (InvokeRequired) 
        Invoke(new MyUpdateCallback(this.MyUpdate), e.MyData);
    else
        this.MyUpdate(e.MyData);
}

// the update function
void MyUpdate(Object myData)
{
    ...
}

这个基本问题:

在使用Invoke工具时,会使用Windows消息队列,它会在队列中放置一条消息,以便等待或发送 - 忘记交叉线程调用,就像发布或发送消息一样。如果在Invoke消息之前有一条消息将使组件及其窗口句柄无效,或者在您尝试执行的任何检查之后放置该消息,那么您将会遇到错误的时间。

 x thread -> PostMessage(WM_CLOSE);   // put 'WM_CLOSE' in queue
 y thread -> this.IsHandleCreated     // yes we have a valid handle
 y thread -> this.Invoke();           // put 'Invoke' in queue
ui thread -> this.Destroy();          // Close processed, handle gone
 y thread -> throw Invalid....()      // 'Send' comes back, thrown on calling thread y

没有真正的方法可以知道控件即将从队列中删除,并且没有什么能够“撤消”调用。无论你做了多少次检查或者你做了额外的锁定,你都无法阻止其他人发出类似关闭或停用的内容。有很多情况可以发生这种情况。

解决方案:

首先要意识到调用将失败,与(IsHandleCreated)检查忽略事件的方式没有什么不同。如果目标是保护非UI线程上的调用者,则需要处理异常,并将其视为未成功的任何其他调用(以防止应用程序崩溃或执行任何操作。除非要重写/ reroll Invoke facility,catch是你唯一知道的方式。

// the event handler. in this case preped for cross thread calls  
void OnEventMyWhatever(object sender, MyUpdateEventArgs e)
{
    if (!this.IsHandleCreated) return;
    if (InvokeRequired) 
    {
        try
        {
            Invoke(new MyUpdateCallback(this.MyUpdate), e.MyData);
        }
        catch (InvalidOperationException ex)    // pump died before we were processed
        {
            if (this.IsHandleCreated) throw;    // not the droids we are looking for
        }
    }
    else
    {
        this.MyUpdate(e.MyData);
    }
}

// the update function
void MyUpdate(Object myData)
{
    ...
}

可以定制异常过滤以满足任何需求。很高兴知道工作线程通常没有所有轻松的外部异常处理和记录UI线程,在大多数应用程序中,所以你可能希望只是吞噬工人端的任何异常。或者记录并重新抛出所有这些。对于许多人来说,工作线程上未被捕获的异常意味着应用程序将崩溃。

答案 4 :(得分:1)

可能是lock(mImageListView){...}?

答案 5 :(得分:1)

您可以使用互斥锁。

线程开头的某处:

 Mutex m=new Mutex();

然后:

if (mImageListView != null && 
    mImageListView.IsHandleCreated &&
    !mImageListView.IsDisposed)
{
    m.WaitOne(); 

    if (mImageListView.InvokeRequired)
        mImageListView.Invoke(
            new RefreshDelegateInternal(mImageListView.RefreshInternal));
    else
        mImageListView.RefreshInternal();

    m.ReleaseMutex();
}

无论你是在处理mImageListView:

 m.WaitOne(); 
 mImageListView.Dispose();
 m.ReleaseMutex();

这应该确保你不能同时处理和调用。

答案 6 :(得分:1)

另见这个问题:

Avoiding the woes of Invoke/BeginInvoke in cross-thread WinForm event handling?

生成EventHandlerForControl的实用程序类可以解决事件方法签名的此问题。您可以调整此课程或查看其中的逻辑来解决问题。

这里真正的问题是nobugz是正确的,因为他指出在winforms中为跨线程调用提供的API本质上不是线程安全的。即使在对InvokeRequired和Invoke / BeginInvoke本身的调用中,也有一些可能导致意外行为的竞争条件。

答案 7 :(得分:1)

如果有可能使用BackGroundWorker,那么有一种非常simple的方法来规避这一点:

public partial class MyForm : Form
{
    private void InvokeViaBgw(Action action)
    {
        BGW.ReportProgress(0, action);
    }

    private void BGW_ProgressChanged(object sender, ProgressChangedEventArgs e)
    {
        if (this.IsDisposed) return; //You are on the UI thread now, so no race condition

        var action = (Action)e.UserState;
        action();
    }

    private private void BGW_DoWork(object sender, DoWorkEventArgs e)
    {
       //Sample usage:
       this.InvokeViaBgw(() => MyTextBox.Text = "Foo");
    }
}

答案 8 :(得分:1)

处理表单结束事件。检查你的off UI线程是否仍在工作,如果是这样,开始将其关闭,取消关闭事件,然后使用表单控件上的BeginInvoke重新安排关闭。

private void Form_FormClosing(object sender, FormClosingEventArgs e)
{
    if (service.IsRunning)
    {
        service.Exit();
        e.Cancel = true;
        this.BeginInvoke(new Action(() => { this.Close(); }));
    }
}

答案 9 :(得分:1)

Isak Savo提出的解决方案

try
  {
  myForm.Invoke(myForm.myDelegate, new Object[] { message });
  }
catch (ObjectDisposedException)
  { //catch exception if the owner window is already closed
  }

在C#4.0中工作但由于某些原因它在C#3.0中失败(无论如何都会引发异常)

所以我使用另一个解决方案,该解决方案基于一个标志,指示表单是否正在关闭,因此如果设置了标志则阻止使用调用

   public partial class Form1 : Form
   {
    bool _closing;
    public bool closing { get { return _closing; } }

    private void Form1_FormClosing(object sender, FormClosingEventArgs e)
    {
        _closing = true;
    }

 ...

 // part executing in another thread: 

 if (_owner.closing == false)
  { // the invoke is skipped if the form is closing
  myForm.Invoke(myForm.myDelegate, new Object[] { message });
  }

这样做的好处是完全避免使用try / catch。

答案 10 :(得分:0)

一种方法可能是调用方法本身而不是调用ImageListView-Method:

if (mImageListView != null && 
    mImageListView.IsHandleCreated &&
    !mImageListView.IsDisposed)
{
    if (mImageListView.InvokeRequired)
        mImageListView.Invoke(new YourDelegate(thisMethod));
    else
        mImageListView.RefreshInternal();
}

这样它会在最终调用RefreshInternal()之前再检查一次。

答案 11 :(得分:0)

停止生成消息的线程的建议是不可接受的。代表可以进行多播。因为一个听众不想听乐队,你不会拍乐队成员。 由于框架没有提供任何简单的方法来清除这些事件消息的消息泵,并且由于表单没有公开其私有属性,让我们知道表单正在关闭: 在取消订阅或停止收听事件后,在窗口的IsClosing事件上设置一个标志,并在执行此操作之前始终检查此标志.Invoke()。

答案 12 :(得分:0)

我有同样的错误。我的错误发生在线程中。最后我写了这个方法:

public bool IsDisposed(Control ctrl)
{
    if (ctrl.IsDisposed)
        return true;
    try
    {
        ctrl.Invoke(new Action(() => { }));
        return false;
    }
    catch (ObjectDisposedException)
    {
        return true;
    }
}

答案 13 :(得分:0)

这对我有用

if (this.IsHandleCreated){
    Task.Delay(500).ContinueWith(_ =>{
        this.Invoke(fm2);
    });
} else {
  this.Refresh();
}