c# - que - Levante eventos en.NET en el hilo principal de la interfaz de usuario
invoke en vb net (8)
Estoy desarrollando una biblioteca de clases en .NET que otros desarrolladores consumirán eventualmente. Esta biblioteca utiliza algunos subprocesos de trabajo y esos subprocesos activan eventos de estado que harán que algunos controles de la interfaz de usuario se actualicen en la aplicación WinForms / WPF.
Normalmente, para cada actualización, deberá verificar la propiedad .InvokeRequired en WinForms o la propiedad equivalente de WPF e invocar esto en el hilo de interfaz de usuario principal para la actualización. Esto puede envejecer rápidamente, y algo no se siente bien al hacer que el desarrollador final haga esto, entonces ...
¿Hay alguna forma en que mi biblioteca pueda activar / invocar los eventos / delegados desde el hilo principal de UI?
En particular...
- ¿Debo "detectar" automáticamente el hilo "principal" para usar?
- Si no es así, ¿debo solicitar al desarrollador final que llame a algún método (pseudo)
UseThisThreadForEvents()
cuando se inicia la aplicación para que pueda tomar el hilo objetivo de esa llamada?
Aquí está la idea de itwolson expresada como un método de extensión que funciona muy bien para mí:
/// <summary>Extension methods for EventHandler-type delegates.</summary>
public static class EventExtensions
{
/// <summary>Raises the event (on the UI thread if available).</summary>
/// <param name="multicastDelegate">The event to raise.</param>
/// <param name="sender">The source of the event.</param>
/// <param name="e">An EventArgs that contains the event data.</param>
/// <returns>The return value of the event invocation or null if none.</returns>
public static object Raise(this MulticastDelegate multicastDelegate, object sender, EventArgs e)
{
object retVal = null;
MulticastDelegate threadSafeMulticastDelegate = multicastDelegate;
if (threadSafeMulticastDelegate != null)
{
foreach (Delegate d in threadSafeMulticastDelegate.GetInvocationList())
{
var synchronizeInvoke = d.Target as ISynchronizeInvoke;
if ((synchronizeInvoke != null) && synchronizeInvoke.InvokeRequired)
{
retVal = synchronizeInvoke.EndInvoke(synchronizeInvoke.BeginInvoke(d, new[] { sender, e }));
}
else
{
retVal = d.DynamicInvoke(new[] { sender, e });
}
}
}
return retVal;
}
}
Entonces simplemente plantee su evento de la siguiente manera:
MyEvent.Raise(this, EventArgs.Empty);
Encontré que confiar en que el método sea un EventHandler no siempre funciona y que ISynchronizeInvoke no funciona para WPF. Mi intento por lo tanto se ve así, puede ayudar a alguien:
public static class Extensions
{
// Extension method which marshals events back onto the main thread
public static void Raise(this MulticastDelegate multicast, object sender, EventArgs args)
{
foreach (Delegate del in multicast.GetInvocationList())
{
// Try for WPF first
DispatcherObject dispatcherTarget = del.Target as DispatcherObject;
if (dispatcherTarget != null && !dispatcherTarget.Dispatcher.CheckAccess())
{
// WPF target which requires marshaling
dispatcherTarget.Dispatcher.BeginInvoke(del, sender, args);
}
else
{
// Maybe its WinForms?
ISynchronizeInvoke syncTarget = del.Target as ISynchronizeInvoke;
if (syncTarget != null && syncTarget.InvokeRequired)
{
// WinForms target which requires marshaling
syncTarget.BeginInvoke(del, new object[] { sender, args });
}
else
{
// Just do it.
del.DynamicInvoke(sender, args);
}
}
}
}
// Extension method which marshals actions back onto the main thread
public static void Raise<T>(this Action<T> action, T args)
{
// Try for WPF first
DispatcherObject dispatcherTarget = action.Target as DispatcherObject;
if (dispatcherTarget != null && !dispatcherTarget.Dispatcher.CheckAccess())
{
// WPF target which requires marshaling
dispatcherTarget.Dispatcher.BeginInvoke(action, args);
}
else
{
// Maybe its WinForms?
ISynchronizeInvoke syncTarget = action.Target as ISynchronizeInvoke;
if (syncTarget != null && syncTarget.InvokeRequired)
{
// WinForms target which requires marshaling
syncTarget.BeginInvoke(action, new object[] { args });
}
else
{
// Just do it.
action.DynamicInvoke(args);
}
}
}
}
Me gustó la respuesta de Mike Bouk (+1) tanto, la incorporé a mi código base. Me preocupa que su llamada de DynamicInvoke genere una excepción de tiempo de ejecución si Delegate invoca no es un delegado de EventHandler, debido a parámetros no coincidentes. Y dado que está en un hilo de fondo, supongo que puede querer llamar al método UI de forma asincrónica y que no le preocupa si alguna vez termina.
Mi versión a continuación solo se puede usar con los delegados de EventHandler e ignorará a otros delegados en su lista de invocación. Como los delegados de EventHandler no devuelven nada, no necesitamos el resultado. Esto me permite llamar a EndInvoke una vez que el proceso asíncrono se completa pasando el EventHandler en la llamada a BeginInvoke. La llamada devolverá este EventHandler en IAsyncResult.AsyncState por medio de AsynchronousCallback, en cuyo momento se llama a EventHandler.EndInvoke.
/// <summary>
/// Safely raises any EventHandler event asynchronously.
/// </summary>
/// <param name="sender">The object raising the event (usually this).</param>
/// <param name="e">The EventArgs for this event.</param>
public static void Raise(this MulticastDelegate thisEvent, object sender,
EventArgs e)
{
EventHandler uiMethod;
ISynchronizeInvoke target;
AsyncCallback callback = new AsyncCallback(EndAsynchronousEvent);
foreach (Delegate d in thisEvent.GetInvocationList())
{
uiMethod = d as EventHandler;
if (uiMethod != null)
{
target = d.Target as ISynchronizeInvoke;
if (target != null) target.BeginInvoke(uiMethod, new[] { sender, e });
else uiMethod.BeginInvoke(sender, e, callback, uiMethod);
}
}
}
private static void EndAsynchronousEvent(IAsyncResult result)
{
((EventHandler)result.AsyncState).EndInvoke(result);
}
Y el uso:
MyEventHandlerEvent.Raise(this, MyEventArgs);
Me gustan estas respuestas y ejemplos, pero intrínsecamente por norma está escribiendo la biblioteca todo mal. Es importante no ordenar sus eventos a otros hilos por el bien de los demás. Mantenga sus eventos disparados donde están y manejados a donde pertenecen. Cuando llega el momento de que ese evento cambie los hilos, es importante dejar que el desarrollador final lo haga en ese momento.
Puede almacenar el despachador para el hilo principal en su biblioteca, usarlo para verificar si se está ejecutando en el hilo de la interfaz de usuario, y ejecutarlo en el hilo de la interfaz de usuario si es necesario.
La documentación de enhebrado de WPF proporciona una buena introducción y ejemplos sobre cómo hacer esto.
Aquí está la esencia de esto:
private Dispatcher _uiDispatcher;
// Call from the main thread
public void UseThisThreadForEvents()
{
_uiDispatcher = Dispatcher.CurrentDispatcher;
}
// Some method of library that may be called on worker thread
public void MyMethod()
{
if (Dispatcher.CurrentDispatcher != _uiDispatcher)
{
_uiDispatcher.Invoke(delegate()
{
// UI thread code
});
}
else
{
// UI thread code
}
}
Puede utilizar la clase SynchronizationContext para realizar llamadas al hilo de la interfaz de usuario en WinForms o WPF utilizando SynchronizationContext.Current
.
Sé que este es un hilo viejo, pero ya que realmente me ayudó a empezar a construir algo similar, así que quiero compartir mi código. Utilizando las nuevas características de C # 7, pude crear una función de Subida consciente de subprocesos. Utiliza la plantilla de delegado EventHandler y la coincidencia de patrón C # 7 y LINQ para filtrar y establecer el tipo.
public static void ThreadAwareRaise<TEventArgs>(this EventHandler<TEventArgs> customEvent,
object sender, TEventArgs e) where TEventArgs : EventArgs
{
foreach (var d in customEvent.GetInvocationList().OfType<EventHandler<TEventArgs>>())
switch (d.Target)
{
case DispatcherObject dispatchTartget:
dispatchTartget.Dispatcher.BeginInvoke(d, sender, e);
break;
case ISynchronizeInvoke syncTarget when syncTarget.InvokeRequired:
syncTarget.BeginInvoke(d, new[] {sender, e});
break;
default:
d.Invoke(sender, e);
break;
}
}
Su biblioteca podría verificar el objetivo de cada delegado en la lista de invocación del evento y coordinar la llamada al hilo de destino si ese objetivo es ISynchronizeInvoke:
private void RaiseEventOnUIThread(Delegate theEvent, object[] args)
{
foreach (Delegate d in theEvent.GetInvocationList())
{
ISynchronizeInvoke syncer = d.Target as ISynchronizeInvoke;
if (syncer == null)
{
d.DynamicInvoke(args);
}
else
{
syncer.BeginInvoke(d, args); // cleanup omitted
}
}
}
Otro enfoque, que hace que el contrato de subprocesos sea más explícito, es exigir a los clientes de su biblioteca que transfieran un ISynchronizeInvoke o SynchronizationContext para el subproceso en el que desean que se generen eventos. Esto les da a los usuarios de su biblioteca un poco más de visibilidad y control que el enfoque de "verificar secretamente el objetivo del delegado".
En lo que respecta a su segunda pregunta, coloque el hilo que reúne las cosas dentro de su OnXxx o cualquier API que el código de usuario llama que podría provocar un evento que se plantea.