suscribirse otro invocar formulario eventos evento event ejecutar disparar desde boton await async agregar c# events asynchronous

otro - invocar evento c#



¿Cómo espero eventos en C#? (6)

Es cierto, los eventos son inherentemente inadmisibles, por lo que tendrá que solucionarlo.

Una solución que he usado en el pasado es usar un semáforo para esperar a que se publiquen todas las entradas. En mi situación, solo tenía un evento suscrito para poder codificarlo como new SemaphoreSlim(0, 1) pero en su caso es posible que desee anular el getter / setter para su evento y mantener un contador de cuántos suscriptores hay para que pueda puede establecer dinámicamente la cantidad máxima de subprocesos simultáneos.

Luego, pasa una entrada de semáforo a cada uno de los suscriptores y les permite hacer lo suyo hasta que SemaphoreSlim.CurrentCount == amountOfSubscribers (también conocido como: todos los espacios han sido liberados).

Esto esencialmente bloquearía su programa hasta que todos los suscriptores del evento hayan terminado.

Es posible que también desee considerar proporcionar un evento a la GameShutDownFinished para sus suscriptores, al que deben llamar cuando hayan terminado su tarea de final del juego. En combinación con la sobrecarga SemaphoreSlim.Release(int) , ahora puede borrar todas las entradas de semáforo y simplemente usar Semaphore.Wait() para bloquear el hilo. En lugar de tener que verificar si todas las entradas se han borrado o no, ahora espera hasta que se haya liberado un lugar (pero solo debería haber un momento en el que todos los lugares se liberen a la vez).

Estoy creando una clase que tiene una serie de eventos, uno de ellos es GameShuttingDown . Cuando se activa este evento, necesito invocar el controlador de eventos. El objetivo de este evento es notificar a los usuarios que el juego se está cerrando y que necesitan guardar sus datos. Las salvadas son esperadas y los eventos no. Entonces, cuando se llama al controlador, el juego se cierra antes de que los controladores esperados puedan completar.

public event EventHandler<EventArgs> GameShuttingDown; public virtual async Task ShutdownGame() { await this.NotifyGameShuttingDown(); await this.SaveWorlds(); this.NotifyGameShutDown(); } private async Task SaveWorlds() { foreach (DefaultWorld world in this.Worlds) { await this.worldService.SaveWorld(world); } } protected virtual void NotifyGameShuttingDown() { var handler = this.GameShuttingDown; if (handler == null) { return; } handler(this, new EventArgs()); }

Registro de eventos

// The game gets shut down before this completes because of the nature of how events work DefaultGame.GameShuttingDown += async (sender, args) => await this.repo.Save(blah);

Entiendo que la firma de los eventos es void EventName y, por lo tanto, hacerlo asíncrono es básicamente disparar y olvidar. Mi motor hace un uso intensivo de eventos para notificar a los desarrolladores de terceros (y múltiples componentes internos) que los eventos tienen lugar dentro del motor y les permite reaccionar ante ellos.

¿Hay una buena ruta para bajar para reemplazar los eventos con algo asíncrono que pueda usar? No estoy seguro de si debería usar BeginShutdownGame y EndShutdownGame con devoluciones de llamada, pero eso es un dolor porque entonces solo la fuente de llamada puede pasar una devolución de llamada, y no cualquier cosa de terceros que se conecte al motor, que es lo que estoy obteniendo con eventos Si el servidor llama a game.ShutdownGame() , no hay forma de que los complementos del motor u otros componentes dentro del motor pasen sus devoluciones de llamada, a menos que conecte algún tipo de método de registro, manteniendo una colección de devoluciones de llamada.

¡Cualquier consejo sobre cuál es la ruta preferida / recomendada para seguir con esto sería muy apreciada! He mirado a mi alrededor y, en su mayor parte, lo que he visto es usar el enfoque de Inicio / Fin, que no creo que satisfaga lo que quiero hacer.

Editar

Otra opción que estoy considerando es usar un método de registro, que requiere una devolución de llamada esperada. Repito sobre todas las devoluciones de llamada, tomo su tarea y espero con un WhenAll .

private List<Func<Task>> ShutdownCallbacks = new List<Func<Task>>(); public void RegisterShutdownCallback(Func<Task> callback) { this.ShutdownCallbacks.Add(callback); } public async Task Shutdown() { var callbackTasks = new List<Task>(); foreach(var callback in this.ShutdownCallbacks) { callbackTasks.Add(callback()); } await Task.WhenAll(callbackTasks); }


Personalmente, creo que tener controladores de eventos async puede no ser la mejor opción de diseño, y el motivo más importante es el problema que estás teniendo. Con los manejadores sincrónicos, es trivial saber cuándo se completan.

Dicho esto, si por alguna razón debes o al menos estar fuertemente obligado a seguir con este diseño, puedes hacerlo de una manera amigable.

Su idea de registrar manejadores y await es buena. Sin embargo, sugeriría seguir con el paradigma de eventos existente, ya que eso mantendrá la expresividad de los eventos en su código. Lo principal es que debe desviarse del tipo de delegado estándar basado en EventHandler y utilizar un tipo de delegado que devuelva una Task para que pueda await los controladores.

Aquí hay un ejemplo simple que ilustra lo que quiero decir:

class A { public event Func<object, EventArgs, Task> Shutdown; public async Task OnShutdown() { Func<object, EventArgs, Task> handler = Shutdown; if (handler == null) { return; } Delegate[] invocationList = handler.GetInvocationList(); Task[] handlerTasks = new Task[invocationList.Length]; for (int i = 0; i < invocationList.Length; i++) { handlerTasks[i] = ((Func<object, EventArgs, Task>)invocationList[i])(this, EventArgs.Empty); } await Task.WhenAll(handlerTasks); } }

El método OnShutdown() , después de hacer el estándar "obtener una copia local de la instancia delegada de eventos", primero invoca todos los controladores y luego espera todas las Tasks devueltas (habiéndolas guardado en una matriz local a medida que se invocan los controladores) .

Aquí hay un breve programa de consola que ilustra el uso:

class Program { static void Main(string[] args) { A a = new A(); a.Shutdown += Handler1; a.Shutdown += Handler2; a.Shutdown += Handler3; a.OnShutdown().Wait(); } static async Task Handler1(object sender, EventArgs e) { Console.WriteLine("Starting shutdown handler #1"); await Task.Delay(1000); Console.WriteLine("Done with shutdown handler #1"); } static async Task Handler2(object sender, EventArgs e) { Console.WriteLine("Starting shutdown handler #2"); await Task.Delay(5000); Console.WriteLine("Done with shutdown handler #2"); } static async Task Handler3(object sender, EventArgs e) { Console.WriteLine("Starting shutdown handler #3"); await Task.Delay(2000); Console.WriteLine("Done with shutdown handler #3"); } }

Después de pasar por este ejemplo, ahora me pregunto si C # no podría haber una forma de resumir esto un poco. Tal vez hubiera sido un cambio demasiado complicado, pero la combinación actual de los controladores de eventos antiguos de devolución de espacios y la nueva función async / wait parece un poco incómoda. Lo anterior funciona (y funciona bien, en mi humilde opinión), pero hubiera sido bueno tener un mejor soporte de CLR y / o lenguaje para el escenario (es decir, poder esperar a un delegado de multidifusión y hacer que el compilador de C # lo convierta en una llamada a WhenAll() ).


Sé que el operador estaba preguntando específicamente sobre el uso asíncrono y las tareas para esto, pero aquí hay una alternativa que significa que los manejadores no necesitan devolver un valor. El código se basa en el ejemplo de Peter Duniho. Primero, la clase A equivalente (aplastada un poco para encajar): -

someInstance.Shutdown += OnShutdown1; someInstance.Shutdown += OnShutdown2; ... private async Task OnShutdown1(SomeClass source, MyEventArgs args) { if (!args.IsProcessed) { // An operation await Task.Delay(123); args.IsProcessed = true; } } private async Task OnShutdown2(SomeClass source, MyEventArgs args) { // OnShutdown2 will start execution the moment OnShutdown1 hits await // and will proceed to the operation, which is not the desired behavior. // Or it can be just a concurrent DB query using the same connection // which can result in an exception thrown base on the provider // and connection string options if (!args.IsProcessed) { // An operation await Task.Delay(123); args.IsProcessed = true; } }

Una aplicación de consola simple para mostrar su uso ...

public static class AsynchronousEventExtensions { public static async Task Raise<TSource, TEventArgs>(this Func<TSource, TEventArgs, Task> handlers, TSource source, TEventArgs args) where TEventArgs : EventArgs { if (handlers != null) { foreach (Func<TSource, TEventArgs, Task> handler in handlers.GetInvocationList()) { await handler(source, args); } } } }

Espero que esto sea útil para alguien.


Si necesita esperar un controlador de eventos .net estándar, no puede hacerlo, porque es void .

Pero puede crear un sistema de eventos asíncrono para manejar eso:

public delegate Task AsyncEventHandler(AsyncEventArgs e); public class AsyncEventArgs : System.EventArgs { public bool Handled { get; set; } } public class AsyncEvent { private string name; private List<AsyncEventHandler> handlers; private Action<string, Exception> errorHandler; public AsyncEvent(string name, Action<string, Exception> errorHandler) { this.name = name; this.handlers = new List<AsyncEventHandler>(); this.errorHandler = errorHandler; } public void Register(AsyncEventHandler handler) { if (handler == null) throw new ArgumentNullException(nameof(handler)); lock (this.handlers) this.handlers.Add(handler); } public void Unregister(AsyncEventHandler handler) { if (handler == null) throw new ArgumentNullException(nameof(handler)); lock (this.handlers) this.handlers.Remove(handler); } public IReadOnlyList<AsyncEventHandler> Handlers { get { var temp = default(AsyncEventHandler[]); lock (this.handlers) temp = this.handlers.ToArray(); return temp.ToList().AsReadOnly(); } } public async Task InvokeAsync() { var ev = new AsyncEventArgs(); var exceptions = new List<Exception>(); foreach (var handler in this.Handlers) { try { await handler(ev).ConfigureAwait(false); if (ev.Handled) break; } catch(Exception ex) { exceptions.Add(ex); } } if (exceptions.Any()) this.errorHandler?.Invoke(this.name, new AggregateException(exceptions)); } }

Y ahora puede declarar sus eventos asíncronos:

public class MyGame { private AsyncEvent _gameShuttingDown; public event AsyncEventHandler GameShuttingDown { add => this._gameShuttingDown.Register(value); remove => this._gameShuttingDown.Unregister(value); } void ErrorHandler(string name, Exception ex) { // handle event error. } public MyGame() { this._gameShuttingDown = new AsyncEvent("GAME_SHUTTING_DOWN", this.ErrorHandler);. } }

E invoque su evento asíncrono usando:

internal async Task NotifyGameShuttingDownAsync() { await this._gameShuttingDown.InvokeAsync().ConfigureAwait(false); }

Versión genérica:

public delegate Task AsyncEventHandler<in T>(T e) where T : AsyncEventArgs; public class AsyncEvent<T> where T : AsyncEventArgs { private string name; private List<AsyncEventHandler<T>> handlers; private Action<string, Exception> errorHandler; public AsyncEvent(string name, Action<string, Exception> errorHandler) { this.name = name; this.handlers = new List<AsyncEventHandler<T>>(); this.errorHandler = errorHandler; } public void Register(AsyncEventHandler<T> handler) { if (handler == null) throw new ArgumentNullException(nameof(handler)); lock (this.handlers) this.handlers.Add(handler); } public void Unregister(AsyncEventHandler<T> handler) { if (handler == null) throw new ArgumentNullException(nameof(handler)); lock (this.handlers) this.handlers.Remove(handler); } public IReadOnlyList<AsyncEventHandler<T>> Handlers { get { var temp = default(AsyncEventHandler<T>[]); lock (this.handlers) temp = this.handlers.ToArray(); return temp.ToList().AsReadOnly(); } } public async Task InvokeAsync(T ev) { var exceptions = new List<Exception>(); foreach (var handler in this.Handlers) { try { await handler(ev).ConfigureAwait(false); if (ev.Handled) break; } catch (Exception ex) { exceptions.Add(ex); } } if (exceptions.Any()) this.errorHandler?.Invoke(this.name, new AggregateException(exceptions)); } }


Peter''s ejemplo Peter''s es genial, lo he simplificado un poco usando LINQ y extensiones:

class A { public delegate void ShutdownEventHandler(EventArgs e); public event ShutdownEventHandler ShutdownEvent; public void OnShutdownEvent(EventArgs e) { ShutdownEventHandler handler = ShutdownEvent; if (handler == null) { return; } Delegate[] invocationList = handler.GetInvocationList(); Parallel.ForEach<Delegate>(invocationList, (hndler) => { ((ShutdownEventHandler)hndler)(e); }); } }

Puede ser una buena idea agregar un tiempo de espera. Para aumentar el evento, llame a la extensión Elevar:

using System; using System.Threading; using System.Threading.Tasks; ... class Program { static void Main(string[] args) { A a = new A(); a.ShutdownEvent += Handler1; a.ShutdownEvent += Handler2; a.ShutdownEvent += Handler3; a.OnShutdownEvent(new EventArgs()); Console.WriteLine("Handlers should all be done now."); Console.ReadKey(); } static void handlerCore( int id, int offset, int num ) { Console.WriteLine("Starting shutdown handler #{0}", id); int step = 200; Thread.Sleep(offset); for( int i = 0; i < num; i += step) { Thread.Sleep(step); Console.WriteLine("...Handler #{0} working - {1}/{2}", id, i, num); } Console.WriteLine("Done with shutdown handler #{0}", id); } static void Handler1(EventArgs e) { handlerCore(1, 7, 5000); } static void Handler2(EventArgs e) { handlerCore(2, 5, 3000); } static void Handler3(EventArgs e) { handlerCore(3, 3, 1000); } }

Pero debe tener en cuenta que, a diferencia de los eventos sincrónicos, esta implementación llama a los controladores simultáneamente. Puede ser un problema si los controladores deben ejecutarse estrictamente consecutivamente, lo que a menudo hacen, por ejemplo, un siguiente controlador depende de los resultados del anterior:

public static class AsynchronousEventExtensions { public static Task Raise<TSource, TEventArgs>(this Func<TSource, TEventArgs, Task> handlers, TSource source, TEventArgs args) where TEventArgs : EventArgs { if (handlers != null) { return Task.WhenAll(handlers.GetInvocationList() .OfType<Func<TSource, TEventArgs, Task>>() .Select(h => h(source, args))); } return Task.CompletedTask; } }

Será mejor que cambie el método de extensión para llamar a los controladores de forma consecutiva:

public event Func<A, EventArgs, Task> Shutdown; private async Task SomeMethod() { ... await Shutdown.Raise(this, EventArgs.Empty); ... }


internal static class EventExtensions { public static void InvokeAsync<TEventArgs>(this EventHandler<TEventArgs> @event, object sender, TEventArgs args, AsyncCallback ar, object userObject = null) where TEventArgs : class { var listeners = @event.GetInvocationList(); foreach (var t in listeners) { var handler = (EventHandler<TEventArgs>) t; handler.BeginInvoke(sender, args, ar, userObject); } } }

ejemplo:

public event EventHandler<CodeGenEventArgs> CodeGenClick; private void CodeGenClickAsync(CodeGenEventArgs args) { CodeGenClick.InvokeAsync(this, args, ar => { InvokeUI(() => { if (args.Code.IsNotNullOrEmpty()) { var oldValue = (string) gv.GetRowCellValue(gv.FocusedRowHandle, nameof(License.Code)); if (oldValue != args.Code) gv.SetRowCellValue(gv.FocusedRowHandle, nameof(License.Code), args.Code); } }); }); }

Nota: Esto es asíncrono, por lo que el controlador de eventos puede comprometer el hilo de la interfaz de usuario. El controlador de eventos (suscriptor) no debe hacer ningún trabajo de interfaz de usuario. No tendría mucho sentido de lo contrario.

  1. declara tu evento en tu proveedor de eventos:

    evento público EventHandler DoSomething;

  2. Invocar evento a su proveedor:

    DoSomething.InvokeAsync (nuevo MyEventArgs (), esto, ar => {devolución de llamada cuando termina (¡sincronice la IU cuando sea necesario aquí!)}, Nulo);

  3. suscriba el evento por cliente como lo haría normalmente