La forma más limpia de invocar eventos de subprocesos cruzados

79

Encuentro que el modelo de eventos .NET es tal que a menudo generaré un evento en un hilo y lo escucharé en otro hilo. Me preguntaba cuál es la forma más limpia de organizar un evento desde un hilo de fondo en mi hilo de interfaz de usuario.

Según las sugerencias de la comunidad, he usado esto:

// earlier in the code
mCoolObject.CoolEvent+= 
           new CoolObjectEventHandler(mCoolObject_CoolEvent);
// then
private void mCoolObject_CoolEvent(object sender, CoolObjectEventArgs args)
{
    if (InvokeRequired)
    {
        CoolObjectEventHandler cb =
            new CoolObjectEventHandler(
                mCoolObject_CoolEvent);
        Invoke(cb, new object[] { sender, args });
        return;
    }
    // do the dirty work of my method here
}
Mella
fuente
Tenga en cuenta que InvokeRequired puede devolver falso cuando un Control administrado existente aún no tiene un identificador no administrado. Debe tener cuidado en los eventos que se plantearán antes de que se haya creado por completo el control.
GregC

Respuestas:

28

Un par de observaciones:

  • No cree delegados simples explícitamente en un código como ese a menos que tenga una versión anterior a 2.0 para poder usar:
   BeginInvoke(new EventHandler<CoolObjectEventArgs>(mCoolObject_CoolEvent), 
               sender, 
               args);
  • Además, no es necesario crear y completar la matriz de objetos porque el parámetro args es un tipo "params", por lo que puede pasar la lista.

  • Yo probablemente favorecen Invokemás BeginInvokeque el último resultado voluntad en el código que se llama de forma asincrónica que puede o no puede ser lo que está buscando, pero haría que el manejo de excepciones posteriores difíciles de propagar sin una llamada a EndInvoke. Lo que sucedería es que su aplicación terminaría obteniendo un TargetInvocationException.

Shaun Austin
fuente
44

Tengo un código para esto en línea. Es mucho mejor que las otras sugerencias; definitivamente échale un vistazo.

Uso de muestra:

private void mCoolObject_CoolEvent(object sender, CoolObjectEventArgs args)
{
    // You could use "() =>" in place of "delegate"; it's a style choice.
    this.Invoke(delegate
    {
        // Do the dirty work of my method here.
    });
}
Domenic
fuente
También puede cambiar el espacio de nombres a System.Windows.Formsen su extensión. De esta manera, evita agregar su espacio de nombres personalizado cada vez que lo necesita.
Joe Almore
10

Evito las declaraciones redundantes de los delegados.

private void mCoolObject_CoolEvent(object sender, CoolObjectEventArgs args)
{
    if (InvokeRequired)
    {
        Invoke(new Action<object, CoolObjectEventArgs>(mCoolObject_CoolEvent), sender, args);
        return;
    }
    // do the dirty work of my method here
}

Para no eventos, puede usar el System.Windows.Forms.MethodInvokerdelegado o System.Action.

EDITAR: Además, cada evento tiene un EventHandlerdelegado correspondiente, por lo que no es necesario volver a declarar uno.

Konrad Rudolph
fuente
1
Para mí funcionó de esta manera:Invoke(new Action<object, CoolObjectEventArgs>(mCoolObject_CoolEvent), sender, args);
António Almeida
@ToniAlmeida Sí, ese fue un error tipográfico en mi código. Gracias por mencionarlo.
Konrad Rudolph
4

Hice la siguiente clase de llamada cruzada 'universal' para mi propio propósito, pero creo que vale la pena compartirla:

using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;

namespace CrossThreadCalls
{
  public static class clsCrossThreadCalls
  {
    private delegate void SetAnyPropertyCallBack(Control c, string Property, object Value);
    public static void SetAnyProperty(Control c, string Property, object Value)
    {
      if (c.GetType().GetProperty(Property) != null)
      {
        //The given property exists
        if (c.InvokeRequired)
        {
          SetAnyPropertyCallBack d = new SetAnyPropertyCallBack(SetAnyProperty);
          c.BeginInvoke(d, c, Property, Value);
        }
        else
        {
          c.GetType().GetProperty(Property).SetValue(c, Value, null);
        }
      }
    }

    private delegate void SetTextPropertyCallBack(Control c, string Value);
    public static void SetTextProperty(Control c, string Value)
    {
      if (c.InvokeRequired)
      {
        SetTextPropertyCallBack d = new SetTextPropertyCallBack(SetTextProperty);
        c.BeginInvoke(d, c, Value);
      }
      else
      {
        c.Text = Value;
      }
    }
  }

Y simplemente puede usar SetAnyProperty () desde otro hilo:

CrossThreadCalls.clsCrossThreadCalls.SetAnyProperty(lb_Speed, "Text", KvaserCanReader.GetSpeed.ToString());

En este ejemplo, la clase KvaserCanReader anterior ejecuta su propio hilo y realiza una llamada para establecer la propiedad de texto de la etiqueta lb_Speed ​​en el formulario principal.

TarPista
fuente
3

Creo que la forma más limpia es definitivamente seguir la ruta AOP. Haga algunos aspectos, agregue los atributos necesarios y nunca más tendrá que verificar la afinidad del hilo.

Dmitri Nesteruk
fuente
No entiendo tu sugerencia. C # no es un lenguaje orientado a aspectos de forma nativa. ¿Tiene en mente algún patrón o biblioteca para implementar aspectos que implemente el marshaling detrás de escena?
Eric
Yo uso PostSharp, por lo que defino el comportamiento de subprocesos en una clase de atributo y luego uso, digamos, el atributo [WpfThread] delante de cada método que se debe llamar en el subproceso de la interfaz de usuario.
Dmitri Nesteruk
3

Utilice el contexto de sincronización si desea enviar un resultado al hilo de la interfaz de usuario. Necesitaba cambiar la prioridad del subproceso, así que cambié de usar subprocesos del grupo de subprocesos (código comentado) y creé un nuevo subproceso propio. Todavía pude usar el contexto de sincronización para devolver si la cancelación de la base de datos tuvo éxito o no.

    #region SyncContextCancel

    private SynchronizationContext _syncContextCancel;

    /// <summary>
    /// Gets the synchronization context used for UI-related operations.
    /// </summary>
    /// <value>The synchronization context.</value>
    protected SynchronizationContext SyncContextCancel
    {
        get { return _syncContextCancel; }
    }

    #endregion //SyncContextCancel

    public void CancelCurrentDbCommand()
    {
        _syncContextCancel = SynchronizationContext.Current;

        //ThreadPool.QueueUserWorkItem(CancelWork, null);

        Thread worker = new Thread(new ThreadStart(CancelWork));
        worker.Priority = ThreadPriority.Highest;
        worker.Start();
    }

    SQLiteConnection _connection;
    private void CancelWork()//object state
    {
        bool success = false;

        try
        {
            if (_connection != null)
            {
                log.Debug("call cancel");
                _connection.Cancel();
                log.Debug("cancel complete");
                _connection.Close();
                log.Debug("close complete");
                success = true;
                log.Debug("long running query cancelled" + DateTime.Now.ToLongTimeString());
            }
        }
        catch (Exception ex)
        {
            log.Error(ex.Message, ex);
        }

        SyncContextCancel.Send(CancelCompleted, new object[] { success });
    }

    public void CancelCompleted(object state)
    {
        object[] args = (object[])state;
        bool success = (bool)args[0];

        if (success)
        {
            log.Debug("long running query cancelled" + DateTime.Now.ToLongTimeString());

        }
    }
El codificador solitario
fuente
2

Siempre me he preguntado qué tan costoso es asumir siempre que se requiere la invocación ...

private void OnCoolEvent(CoolObjectEventArgs e)
{
  BeginInvoke((o,e) => /*do work here*/,this, e);
}

fuente
1
Realizar un BeginInvoke dentro de un hilo de la GUI hará que la acción en cuestión se posponga hasta la próxima vez que el hilo de la interfaz de usuario procese los mensajes de Windows. En realidad, esto puede ser útil en algunos casos.
supercat
2

Como nota al margen interesante, el enlace de WPF maneja la clasificación automáticamente para que pueda vincular la interfaz de usuario a las propiedades del objeto que se modifican en subprocesos en segundo plano sin tener que hacer nada especial. Esto ha demostrado ser un gran ahorro de tiempo para mí.

En XAML:

<TextBox Text="{Binding Path=Name}"/>
gbc
fuente
esto no funcionará. una vez que estableces el prop en el subproceso que no es de UI, obtienes una excepción ... es decir, Name = "gbc" bang! fracaso ... no hay mate de queso gratis
Boppity Bop
No es gratis (cuesta tiempo de ejecución), pero la maquinaria de enlace de wpf parece manejar automáticamente la clasificación de subprocesos cruzados. Usamos mucho esto con accesorios que se actualizan mediante datos de red recibidos en subprocesos en segundo plano. Hay una explicación aquí: blog.lab49.com/archives/1166
gbc
1
@gbc Aaaaand explicación desaparecida 404.
Jan 'splite' K.
0

Puede intentar desarrollar algún tipo de componente genérico que acepte un SynchronizationContext como entrada y lo use para invocar los eventos.

En Freund
fuente