2008-08-08 8 views
32

Cuando se suscribe a un evento en un objeto desde un formulario, esencialmente le está entregando el control de su método de devolución de llamada al origen del evento. No tiene idea si esa fuente de eventos elegirá activar el evento en un hilo diferente.¿Cómo puedo hacer que las devoluciones de eventos en mis formularios de win sean seguras?

El problema es que cuando se invoca la devolución de llamada, no puede suponer que puede realizar controles de actualización en su formulario porque a veces esos controles arrojarán una expección si la devolución de llamada de evento fue invocada en un hilo diferente al del formulario ejecutarse en.

Respuesta

31

Para simplificar un poco el código de Simon, puede usar el delegado de acción genérico incorporado. Le ahorra a su código un montón de tipos de delegados que realmente no necesita. Además, en .NET 3.5 agregaron un parámetro params al método Invoke para que no tenga que definir una matriz temporal.

void SomethingHappened(object sender, EventArgs ea) 
{ 
    if (InvokeRequired) 
    { 
     Invoke(new Action<object, EventArgs>(SomethingHappened), sender, ea); 
     return; 
    } 

    textBox1.Text = "Something happened"; 
} 
15

Éstos son los puntos más destacados:

  1. No se pueden realizar llamadas de control de interfaz de usuario de un hilo diferente a la que fueron creados en (rosca de la forma).
  2. Las invocaciones de delegado (es decir, anzuelos de evento) se desencadenan en el mismo subproceso que el objeto que está disparando el evento.

Por lo tanto, si tiene un hilo de "motor" separado haciendo algún trabajo y tiene alguna interfaz de usuario para ver los cambios de estado que se pueden reflejar en la IU (como una barra de progreso o algo así), tiene un problema. El incendio del motor es un evento modificado por un objeto que ha sido enganchado por el Formulario. Pero el delegado de devolución de llamada que llama al Formulario registrado con el motor en el hilo del motor ... no en el hilo del Formulario. Y entonces no puedes actualizar ningún control de esa devolución de llamada. Doh!

BeginInvoke viene al rescate. Sólo tiene que utilizar este modelo de codificación simple en todas sus métodos de devolución de llamada y usted puede estar seguro de que las cosas van a estar bien:

private delegate void EventArgsDelegate(object sender, EventArgs ea); 

void SomethingHappened(object sender, EventArgs ea) 
{ 
    // 
    // Make sure this callback is on the correct thread 
    // 
    if (this.InvokeRequired) 
    { 
     this.Invoke(new EventArgsDelegate(SomethingHappened), new object[] { sender, ea }); 
     return; 
    } 

    // 
    // Do something with the event such as update a control 
    // 
    textBox1.Text = "Something happened"; 
} 

Es bastante simple en realidad.

  1. Uso InvokeRequired para averiguar si esta devolución de llamada que sucedió en la secuencia correcta.
  2. De lo contrario, reinicie la devolución de llamada en la secuencia correcta con los mismos parámetros. Puede reinvocar un método utilizando los métodos Invoke (bloqueo) o BeginInvoke (sin bloqueo).
  3. La próxima vez que se llame a la función, InvokeRequired devuelve falso porque ahora estamos en el hilo correcto y todo el mundo está contento.

Esta es una forma muy compacta de hacer frente a este problema y hacer salvo de las devoluciones de llamada de eventos multi-hilo sus formularios.

+1

Generalmente prefiero BeginInvoke a Invoke, pero hay una advertencia: uno debe evitar hacer cola en demasiados eventos. Utilizo una variable updateRequired que está configurada en 1 cuando ocurriría un BeginInvoke, y solo realizo el BeginInvoke si hubiera sido cero (usando Interlocked.Exchange). El controlador de pantalla tiene un ciclo while que borra updateRequired y, si no era cero, realiza una actualización y realiza un bucle. En algunos casos, se agrega un temporizador para limitar aún más la frecuencia de actualización (para evitar que el código gaste todo el tiempo actualizando la lectura del progreso en lugar de hacer un trabajo real), pero eso es más complicado. – supercat

+0

@Supercat ... la aceleración de eventos es un tema importante para muchas aplicaciones, pero no es algo que deba ser parte de la capa de UI. Se debe crear un bus proxy de eventos separado para recibir, poner en cola, combinar y reenviar eventos a intervalos apropiados. Cualquier suscriptor al bus de eventos no debe saber que está ocurriendo un límite de eventos. –

+0

Puedo ver lugares donde podría ser útil un "bus de eventos" separado para manejar la sincronización, pero en muchos casos parecería más fácil para el usuario final algo así como una clase de indicador de progreso si la clase simplemente expuso una propiedad MinimumUpdateInterval. – supercat

0

En muchos casos simples, puede usar el delegado de MethodInvoker y evitar la necesidad de crear su propio tipo de delegado.

9

utilizo métodos anónimos mucho en este escenario:

void SomethingHappened(object sender, EventArgs ea) 
{ 
    MethodInvoker del = delegate{ textBox1.Text = "Something happened"; }; 
    InvokeRequired ? Invoke(del) : del(); 
} 
2

estoy un poco tarde a este tema, pero es posible que desee echar un vistazo a la Event-Based Asynchronous Pattern. Cuando se implementa correctamente, garantiza que los eventos siempre se generan desde el hilo de la interfaz de usuario.

Aquí hay un breve ejemplo que solo permite una invocación simultánea; El soporte de múltiples invocaciones/eventos requiere un poco más de plomería.

using System; 
using System.ComponentModel; 
using System.Threading; 
using System.Windows.Forms; 

namespace WindowsFormsApplication1 
{ 
    public class MainForm : Form 
    { 
     private TypeWithAsync _type; 

     [STAThread()] 
     public static void Main() 
     { 
      Application.EnableVisualStyles(); 
      Application.Run(new MainForm()); 
     } 

     public MainForm() 
     { 
      _type = new TypeWithAsync(); 
      _type.DoSomethingCompleted += DoSomethingCompleted; 

      var panel = new FlowLayoutPanel() { Dock = DockStyle.Fill }; 

      var btn = new Button() { Text = "Synchronous" }; 
      btn.Click += SyncClick; 
      panel.Controls.Add(btn); 

      btn = new Button { Text = "Asynchronous" }; 
      btn.Click += AsyncClick; 
      panel.Controls.Add(btn); 

      Controls.Add(panel); 
     } 

     private void SyncClick(object sender, EventArgs e) 
     { 
      int value = _type.DoSomething(); 
      MessageBox.Show(string.Format("DoSomething() returned {0}.", value)); 
     } 

     private void AsyncClick(object sender, EventArgs e) 
     { 
      _type.DoSomethingAsync(); 
     } 

     private void DoSomethingCompleted(object sender, DoSomethingCompletedEventArgs e) 
     { 
      MessageBox.Show(string.Format("DoSomethingAsync() returned {0}.", e.Value)); 
     } 
    } 

    class TypeWithAsync 
    { 
     private AsyncOperation _operation; 

     // synchronous version of method 
     public int DoSomething() 
     { 
      Thread.Sleep(5000); 
      return 27; 
     } 

     // async version of method 
     public void DoSomethingAsync() 
     { 
      if (_operation != null) 
      { 
       throw new InvalidOperationException("An async operation is already running."); 
      } 

      _operation = AsyncOperationManager.CreateOperation(null); 
      ThreadPool.QueueUserWorkItem(DoSomethingAsyncCore); 
     } 

     // wrapper used by async method to call sync version of method, matches WaitCallback so it 
     // can be queued by the thread pool 
     private void DoSomethingAsyncCore(object state) 
     { 
      int returnValue = DoSomething(); 
      var e = new DoSomethingCompletedEventArgs(returnValue); 
      _operation.PostOperationCompleted(RaiseDoSomethingCompleted, e); 
     } 

     // wrapper used so async method can raise the event; matches SendOrPostCallback 
     private void RaiseDoSomethingCompleted(object args) 
     { 
      OnDoSomethingCompleted((DoSomethingCompletedEventArgs)args); 
     } 

     private void OnDoSomethingCompleted(DoSomethingCompletedEventArgs e) 
     { 
      var handler = DoSomethingCompleted; 

      if (handler != null) { handler(this, e); } 
     } 

     public EventHandler<DoSomethingCompletedEventArgs> DoSomethingCompleted; 
    } 

    public class DoSomethingCompletedEventArgs : EventArgs 
    { 
     private int _value; 

     public DoSomethingCompletedEventArgs(int value) 
      : base() 
     { 
      _value = value; 
     } 

     public int Value 
     { 
      get { return _value; } 
     } 
    } 
} 
+1

Creo que es un poco engañoso decir "garantiza que los eventos siempre se generan desde el hilo de la interfaz de usuario". ¿No sería más exacto decir que garantiza que el controlador de eventos se ejecute en el mismo SynchronizationContext/thread en el que se creó la tarea? (Que podría no ser el subproceso UI/SynchronizationContext) – jspaey

1

A medida que el lazy programmer, tengo un método muy perezoso de hacer esto.

Lo que hago es simplemente esto.

private void DoInvoke(MethodInvoker del) { 
    if (InvokeRequired) { 
     Invoke(del); 
    } else { 
     del(); 
    } 
} 
//example of how to call it 
private void tUpdateLabel(ToolStripStatusLabel lbl, String val) { 
    DoInvoke(delegate { lbl.Text = val; }); 
} 

Puede alinear el DoInvoke dentro de su función u ocultarlo en una función separada para hacer el trabajo sucio por usted.

Solo tenga en cuenta que puede pasar funciones directamente al método DoInvoke.

private void directPass() { 
    DoInvoke(this.directInvoke); 
} 
private void directInvoke() { 
    textLabel.Text = "Directly passed."; 
} 
+0

Estoy a favor de la programación diferida :) Si está usando .NET 3.5 o superior, puede usar 'Action' o' Action 'junto con lambda expresiones: 'Doinvoke ((= => textLabel.Text =" Algo ")' –

Cuestiones relacionadas