Invoke o BeginInvoke no se pueden llamar en un control hasta que se haya creado el identificador de ventana

81

Tengo un método de extensión SafeInvoke Control similar al que Greg D analiza aquí (menos la verificación IsHandleCreated).

Lo estoy llamando de la System.Windows.Forms.Formsiguiente manera:

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

A veces (esta llamada puede provenir de una variedad de hilos) esto da como resultado el siguiente error:

System.InvalidOperationException ocurrió

Message= "No se puede llamar a Invoke o BeginInvoke en un control hasta que se haya creado el identificador de ventana."

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

¿Qué está pasando y cómo lo soluciono? Sé que no es un problema de creación de formularios, ya que a veces funcionará una vez y fallará la próxima vez, entonces, ¿cuál podría ser el problema?

PD. Realmente soy horrible en WinForms, ¿alguien conoce una buena serie de artículos que explique todo el modelo y cómo trabajar con él?

George Mauer
fuente
1
Algo extraño está sucediendo con el enlace ... el marcado y la vista previa son correctos ... extraño.
George Mauer
¿En qué contextos se llama Show? ¿Alguna vez se llama desde el constructor de un formulario, por ejemplo? Puede ser útil registrar mensajes para que las llamadas se muestren en los mensajes activados por el evento HandleCreated para verificar que solo está llamando a mostrar en objetos que ya tienen sus identificadores creados.
Greg D
¿Para qué es la aplicación / cómo está diseñada? ¿Qué hace this.Show ()? (Supongo que hace algo más que esto. Visible = true;) ¿Es su referencia a los formularios web un error tipográfico?
Greg D
this.Show () es el Form.Show () base, así que lo que sea que haga. El diálogo nunca se abre desde un constructor. Se llama mediante una implementación de un servicio INotifier que tiene un método de notificación simple (cadena)
George Mauer
4
Mirándolo de nuevo, más de un año después, parece que está experimentando el error precisamente por la razón por la que IsHandleCreatedexiste la verificación. Está intentando cambiar una propiedad de (Enviar un mensaje a) un control que aún no se ha creado. Una cosa que puede hacer en esta situación es poner en cola a los delegados que se envían antes de la creación del control y luego ejecutarlos en el HandleCreatedevento.
Greg D

Respuestas:

76

Es posible que esté creando sus controles en el hilo incorrecto. Considere la siguiente documentación de MSDN :

Esto significa que InvokeRequired puede devolver falso si no se requiere Invoke (la llamada ocurre en el mismo hilo), o si el control se creó en un hilo diferente pero el identificador del control aún no se ha creado.

En el caso de que el identificador del control aún no se haya creado, no debe simplemente llamar a propiedades, métodos o eventos en el control. Esto puede hacer que el identificador del control se cree en el hilo de fondo, aislando el control en un hilo sin un bombeo de mensajes y haciendo que la aplicación sea inestable.

Puede protegerse contra este caso comprobando también el valor de IsHandleCreated cuando InvokeRequired devuelve falso en un hilo en segundo plano. Si el identificador de control aún no se ha creado, debe esperar hasta que se haya creado antes de llamar a Invoke o BeginInvoke. Normalmente, esto sucede solo si se crea un hilo en segundo plano en el constructor del formulario principal para la aplicación (como en Application.Run (new MainForm ()), antes de que se haya mostrado el formulario o se haya llamado a Application.Run.

Veamos qué significa esto para ti. (Esto sería más fácil de razonar si también viéramos su implementación de SafeInvoke)

Suponiendo que su implementación es idéntica a la referenciada con la excepción de la verificación contra IsHandleCreated , sigamos la lógica:

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

Considere el caso en el que llamamos SafeInvokedesde el subproceso no gui para un control cuyo identificador no se ha creado.

uiElementno es nulo, así que lo comprobamos uiElement.InvokeRequired. Según los documentos de MSDN (en negrita) InvokeRequiredvolverá falseporque, aunque se creó en un hilo diferente, ¡el identificador no se ha creado! Esto nos envía a la elsecondición donde verificamos IsDisposedo inmediatamente procedemos a llamar a la acción enviada ... ¡ desde el hilo de fondo !

En este punto, todas las apuestas están desactivadas con respecto a ese control porque su identificador se ha creado en un hilo que no tiene una bomba de mensajes para él, como se menciona en el segundo párrafo. ¿Quizás este es el caso que estás encontrando?

Greg D
fuente
¿Debería incluir un EndInvokedespués del BeginInvoke?
Odys
@odyodyodys: Respuesta corta: No. Este es un caso mágico y súper específico en el que no es necesario. Respuesta más larga: lea los comentarios sobre esta respuesta: stackoverflow.com/a/714680/6932
Greg D
1
Esta respuesta y el artículo de MSDN tratan sobre que InvokeRequired devuelva falso porque no se crea el identificador. Pero OP está obteniendo una excepción cuando se llama a Beginvoke / Invoke después de que InvokeRequired devuelve verdadero. ¿Cómo puede InvokeRequired devolver verdadero cuando el identificador aún no se ha creado?
thewpfguy
También hay una condición de carrera, una que he corrido, en wrt IsDisposed. IsDisposed puede ser falso cuando se prueba, pero convertirse en verdadero antes de que la acción enviada se ejecute por completo. Las dos opciones parecen ser (a) ignorar InvalidOperationException y (b) usar bloqueo para crear secciones críticas a partir de la acción enviada y el método de eliminación de control. El primero se siente como un truco y el segundo es un dolor.
blearyeye
36

Encontré el InvokeRequiredno confiable, así que simplemente uso

if (!this.IsHandleCreated)
{
    this.CreateHandle();
}
Mathieu
fuente
5
¿No podría esto causar que se estén creando dos identificadores en diferentes subprocesos? El identificador debería crearse, solo tienes que mejorar el tiempo / orden de los eventos ..
Denise Skidmore
Niza - Yo prefiero esto a solo acceder a this.Handle como (a) no tiene una variable sin usar y (b) que es obvio lo que está pasando
Dunc
5
MSDN: "En el caso de que el identificador del control aún no se haya creado, no debe simplemente llamar a propiedades, métodos o eventos en el control. Esto puede hacer que el identificador del control se cree en el subproceso en segundo plano, aislando el control en un hilo sin un mensaje de bomba y haciendo que la aplicación sea inestable ". El objetivo del ejercicio es evitar crear el mango en el hilo equivocado. Si esta llamada ocurre desde un hilo que no es el hilo de la interfaz gráfica de usuario, bang, estás muerto.
Greg D
25

Aquí está mi respuesta a una pregunta similar :

Yo creo (todavía no del todo seguro) que esto se debe InvokeRequired siempre devolverá false si el control aún no se ha cargado / muestra. He hecho una solución que parece funcionar por el momento, que es simplemente hacer referencia al identificador del control asociado en su creador, así:

var x = this.Handle; 

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

Benjol
fuente
Artículo muy interesante por cierto. Gracias.
Yann Trevin
Gracias, esto funcionó para mí porque tenía un formulario oculto que necesitaba ser animado dentro y fuera de un hilo de fondo. Hacer referencia al mango fue lo que hizo que funcionara para mí
John Mc
Esto sigue siendo un problema en las últimas versiones de .net, aunque es menos un error que una "característica". Vale la pena señalar que poner un "reloj" en el objeto y examinar sus propiedades también hace lo mismo que mirar el identificador. Terminas con algunas tonterías de depuración cuántica donde funciona cuando lo miras.
Tony Cheetham
5

El método en la publicación que vincula a las llamadas Invoke/ BeginInvokeantes de verificar si el identificador del control se ha creado en el caso de que se llame desde un hilo que no creó el control.

Entonces obtendrá la excepción cuando su método sea llamado desde un hilo que no sea el que creó el control. Esto puede suceder por eventos remotos o elementos de usuario de trabajo en cola ...

EDITAR

Si marca InvokeRequiredy HandleCreatedantes de llamar a invoke, no debería obtener esa excepción.

Ella a
fuente
Si lo entiendo correctamente, estás diciendo que esto sucederá siempre que el hilo de invocación sea diferente al que se creó el control. No puedo garantizar desde qué hilo se llamará el evento. Podría ser el que lo creó es (más probable) es un hilo completamente diferente. ¿Cómo resuelvo esto?
George Mauer
sí, eso es correcto. Edité la publicación con una condición que debería solucionar el problema.
Shea
No estoy convencido de que este sea el caso. Actualicé mi pregunta en base a tu comentario, Arnshea.
Greg D
No entiendo. Necesito que se muestre esa ventana, no tengo claro por qué IsHandleCreated es falso, pero que no se muestre la ventana no es una opción, mi pregunta es acerca de por qué en el mundo sería falso
George Mauer
Creo que IsHandleCreated devolverá falso si el identificador se ha cerrado / el control se ha eliminado. ¿Está seguro de que no está siendo mordido por una invocación asincrónica en un control que solía existir, pero que ya no existe?
Greg D
3

Si va a utilizar un Controlde otro hilo antes de mostrar o hacer otras cosas con el Control, considere forzar la creación de su identificador dentro del constructor. Esto se hace usando la CreateHandlefunción.

En un proyecto de subprocesos múltiples, donde la lógica del "controlador" no está en un WinForm, esta función es fundamental en los Controlconstructores para evitar este error.

Expiación limitada
fuente
3

Agregue esto antes de llamar al método invoke:

while (!this.IsHandleCreated) 
   System.Threading.Thread.Sleep(100)
amos godwin
fuente
1

Haga referencia al identificador del control asociado en su creador, así:

Nota : Tenga cuidado con esta solución. Si un control tiene un identificador, es mucho más lento hacer cosas como establecer el tamaño y la ubicación del mismo. Esto hace que InitializeComponent sea mucho más lento. Una mejor solución es no poner nada en segundo plano antes de que el control tenga un identificador.

Joe
fuente
0

Tuve este problema con este tipo de formulario simple:

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

Luego, para notros subprocesos asíncronos que estaba usando new MyForm().UpdateLabel(text)para intentar llamar al subproceso de la interfaz de usuario, pero el constructor no le da ningún identificador a la instancia del subproceso de la interfaz de usuario, por lo que otros subprocesos obtienen otros identificadores de instancia, que son Object reference not set to an instance of an objecto Invoke or BeginInvoke cannot be called on a control until the window handle has been created. Para resolver esto, usé un objeto estático para sostener el identificador de la interfaz de usuario:

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

Supongo que está funcionando bien, hasta ahora ...

rupweb
fuente
0
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();
Shimon Doodkin
fuente
esto es c # - thisno varía según la invocación, esa técnica de estilo javascript debería ser innecesaria.
George Mauer
Seguro que traté de hacer explícito en qué invocar. - lo que sea
Shimon Doodkin
0

¿Qué pasa con esto?


    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;
    }
Gourou Dsecours
fuente