在创建窗口句柄之前,不能对控件调用 Invoke 或 BeginInvoke

我有一个类似于 格雷格 · D 在这里讨论(减去 IsHandleCreated 检查)的 SafeInvoke Control 扩展方法。

我从 System.Windows.Forms.Form打电话给你,内容如下:

public void Show(string text) {
label.SafeInvoke(()=>label.Text = text);
this.Show();
this.Refresh();
}

有时(这个调用可以来自各种线程) ,这会导致以下错误:

发生 System.InvalidOperationException

在创建窗口句柄之前,不能对控件调用 Invoke 或 BeginInvoke

Source = “ System. Windows. Forms”

StackTrace:
at System.Windows.Forms.Control.MarshaledInvoke(Control caller, Delegate method, Object[] args, Boolean synchronous)
at System.Windows.Forms.Control.Invoke(Delegate method, Object[] args)
at System.Windows.Forms.Control.Invoke(Delegate method)
at DriverInterface2.UI.WinForms.Dialogs.FormExtensions.SafeInvoke[T](T control, Action`1 action)
in C:\code\DriverInterface2\DriverInterface2.UI.WinForms\Dialogs\FormExtensions.cs:line 16

这是怎么回事,我该怎么补救?我知道这不是一个形式创造的问题,因为有时候它会工作一次,失败的下一次所以问题可能是什么?

附言。我真的很不擅长使用 WinForms,有人知道一系列解释整个模型以及如何使用它的文章吗?

152340 次浏览

The method in the post you link to calls Invoke/BeginInvoke before checking if the control's handle has been created in the case where it's being called from a thread that didn't create the control.

So you'll get the exception when your method is called from a thread other than the one that created the control. This can happen from remoting events or queued work user items...

EDIT

If you check InvokeRequired and HandleCreated before calling invoke you shouldn't get that exception.

It's possible that you're creating your controls on the wrong thread. Consider the following documentation from MSDN:

This means that InvokeRequired can return false if Invoke is not required (the call occurs on the same thread), or if the control was created on a different thread but the control's handle has not yet been created.

In the case where the control's handle has not yet been created, you should not simply call properties, methods, or events on the control. This might cause the control's handle to be created on the background thread, isolating the control on a thread without a message pump and making the application unstable.

You can protect against this case by also checking the value of IsHandleCreated when InvokeRequired returns false on a background thread. If the control handle has not yet been created, you must wait until it has been created before calling Invoke or BeginInvoke. Typically, this happens only if a background thread is created in the constructor of the primary form for the application (as in Application.Run(new MainForm()), before the form has been shown or Application.Run has been called.

Let's see what this means for you. (This would be easier to reason about if we saw your implementation of SafeInvoke also)

Assuming your implementation is identical to the referenced one with the exception of the check against IsHandleCreated, let's follow the logic:

public static void SafeInvoke(this Control uiElement, Action updater, bool forceSynchronous)
{
if (uiElement == null)
{
throw new ArgumentNullException("uiElement");
}


if (uiElement.InvokeRequired)
{
if (forceSynchronous)
{
uiElement.Invoke((Action)delegate { SafeInvoke(uiElement, updater, forceSynchronous); });
}
else
{
uiElement.BeginInvoke((Action)delegate { SafeInvoke(uiElement, updater, forceSynchronous); });
}
}
else
{
if (uiElement.IsDisposed)
{
throw new ObjectDisposedException("Control is already disposed.");
}


updater();
}
}

Consider the case where we're calling SafeInvoke from the non-gui thread for a control whose handle has not been created.

uiElement is not null, so we check uiElement.InvokeRequired. Per the MSDN docs (bolded) InvokeRequired will return false because, even though it was created on a different thread, the handle hasn't been created! This sends us to the else condition where we check IsDisposed or immediately proceed to call the submitted action... from the background thread!

At this point, all bets are off re: that control because its handle has been created on a thread that doesn't have a message pump for it, as mentioned in the second paragraph. Perhaps this is the case you're encountering?

Here is my answer to a similar question:

I think (not yet entirely sure) that this is because InvokeRequired will always return false if the control has not yet been loaded/shown. I have done a workaround which seems to work for the moment, which is to simple reference the handle of the associated control in its creator, like so:

var x = this.Handle;

(See http://ikriv.com/en/prog/info/dotnet/MysteriousHang.html)

Reference the handle of the associated control in its creator, like so:

Note: Be wary of this solution.If a control has a handle it is much slower to do things like set the size and location of it. This makes InitializeComponent much slower. A better solution is to not background anything before the control has a handle.

If you're going to use a Control from another thread before showing or doing other things with the Control, consider forcing the creation of its handle within the constructor. This is done using the CreateHandle function.

In a multi-threaded project, where the "controller" logic isn't in a WinForm, this function is instrumental in Control constructors for avoiding this error.

I found the InvokeRequired not reliable, so I simply use

if (!this.IsHandleCreated)
{
this.CreateHandle();
}

I had this problem with this kind of simple form:

public partial class MyForm : Form
{
public MyForm()
{
Load += new EventHandler(Form1_Load);
}


private void Form1_Load(Object sender, EventArgs e)
{
InitializeComponent();
}


internal void UpdateLabel(string s)
{
Invoke(new Action(() => { label1.Text = s; }));
}
}

Then for n other async threads I was using new MyForm().UpdateLabel(text) to try and call the UI thread, but the constructor gives no handle to the UI thread instance, so other threads get other instance handles, which are either Object reference not set to an instance of an object or Invoke or BeginInvoke cannot be called on a control until the window handle has been created. To solve this I used a static object to hold the UI handle:

public partial class MyForm : Form
{
private static MyForm _mf;


public MyForm()
{
Load += new EventHandler(Form1_Load);
}


private void Form1_Load(Object sender, EventArgs e)
{
InitializeComponent();
_mf = this;
}


internal void UpdateLabel(string s)
{
_mf.Invoke((MethodInvoker) delegate { _mf.label1.Text = s; });
}
}

I guess it's working fine, so far...

var that = this; // this is a form
(new Thread(()=> {


var action= new Action(() => {
something
}));


if(!that.IsDisposed)
{
if(that.IsHandleCreated)
{
//if (that.InvokeRequired)
that.BeginInvoke(action);
//else
// action.Invoke();
}
else
that.HandleCreated+=(sender,event) => {
action.Invoke();
};
}




})).Start();

Add this before you call method invoke:

while (!this.IsHandleCreated)
System.Threading.Thread.Sleep(100)

What about this :


public static bool SafeInvoke( this Control control, MethodInvoker method )
{
if( control != null && ! control.IsDisposed && control.IsHandleCreated && control.FindForm().IsHandleCreated )
{
if( control.InvokeRequired )
{
control.Invoke( method );
}
else
{
method();
}
return true;
}
else return false;
}