Cómo utilizar Observable.FromEvent en lugar de FromEventPattern y evitar nombres de eventos literales de cadena

Estoy aprendiendo acerca de Rx dentro de WinForms, y tengo el siguiente código:

// Create an observable from key presses, grouped by the key pressed var groupedKeyPresses = Observable.FromEventPattern(this, "KeyPress") .Select(k => k.EventArgs.KeyChar) .GroupBy(k => k); // Increment key counter and update user's display groupedKeyPresses.Subscribe(keyPressGroup => { var numPresses = 0; keyPressGroup.Subscribe(key => UpdateKeyPressStats(key, ++numPresses)); }); 

Esto funciona / funciona perfectamente, se transmite en los eventos de KeyPress, agrupa la tecla presionada, y luego realiza un seguimiento de cuántas veces se presionó cada tecla y llama a un método UpdateKeyPressStats con la tecla y el nuevo número de pulsaciones. ¡Envíalo!

Sin embargo, no soy seguidor de la firma FromEventPattern , debido a la referencia literal de cadena al evento. Entonces, pensé que probaría FromEvent en FromEvent lugar.

 // Create an observable from key presses, grouped by the key pressed var groupedKeyPresses = Observable.FromEvent(h => this.KeyPress += h, h => this.KeyPress -= h) .Select(k => k.KeyChar) .GroupBy(k => k); // Increment key counter and update user's display groupedKeyPresses.Subscribe(keyPressGroup => { var numPresses = 0; keyPressGroup.Subscribe(key => UpdateKeyPressStats(key, ++numPresses)); }); 

Entonces, el único cambio fue intercambiar Observable.FromEventPattern con Observable.FromEvent (y la ruta en la consulta Select LINQ para obtener KeyChar ). El rest, incluidos los métodos de Subscribe , son idénticos. Sin embargo, en el tiempo de ejecución con la segunda solución, obtengo:

Se produjo una excepción no controlada del tipo ‘System.ArgumentException’ en mscorlib.dll

Información adicional: No se puede enlazar al método de destino porque su firma o transparencia de seguridad no es compatible con la del tipo de delegado.

¿Qué está causando esta excepción de tiempo de ejecución y cómo debo evitarla?

  • GUI: WinForms
  • Versión de Rx & Rx-WinForms: 2.1.30214.0 (a través de Nuget)
  • Marco objective: 4.5

Resumen

El primer punto que debe hacer es que no necesita usar Observable.FromEvent para evitar la referencia literal de cadena. Esta versión de FromEventPattern funcionará:

 var groupedKeyPresses = Observable.FromEventPattern( h => KeyPress += h, h => KeyPress -= h) .Select(k => k.EventArgs.KeyChar) .GroupBy(k => k); 

Si desea hacer que FromEvent funcione, puede hacerlo así:

 var groupedKeyPresses = Observable.FromEvent( handler => { KeyPressEventHandler kpeHandler = (sender, e) => handler(e); return kpeHandler; }, h => KeyPress += h, h => KeyPress -= h) .Select(k => k.KeyChar) .GroupBy(k => k); 

¿Por qué? Es porque el operador FromEvent existe para trabajar con cualquier tipo de delegado de evento.

El primer parámetro aquí es una función de conversión que conecta el evento al suscriptor Rx. Acepta el controlador OnNext de un observador (una Action ) y devuelve un controlador compatible con el delegado de evento subyacente que invocará ese controlador OnNext. Este controlador generado se puede suscribir al evento.

Nunca me gustó la documentación oficial de MSDN para esta función , así que aquí hay una explicación expandida que recorre el uso de esta función pieza por pieza.

The Lowdown en Observable.FromEvent

A continuación, se FromEvent por qué existe FromEvent y cómo funciona:

Revisión de cómo funcionan las suscripciones a eventos .NET

Considera cómo funcionan los eventos .NET. Estos se implementan como cadenas de delegates. Los delegates de eventos estándar siguen el patrón de delegate void FooHandler(object sender, EventArgs eventArgs) , pero en realidad los eventos pueden funcionar con cualquier tipo de delegado (incluso aquellos con un tipo de devolución). Nos suscribimos a un evento pasando un delegado apropiado a una función especial que lo agrega a una cadena de delegates (generalmente a través del operador + =), o si todavía no hay suscriptores suscritos, el delegado se convierte en la raíz de la cadena. Es por eso que debemos hacer una comprobación nula cuando se plantea un evento.

Cuando se produce el evento, (típicamente) se invoca la cadena de delegado para que cada delegado en la cadena sea llamado sucesivamente. Para darse de baja de un evento .NET, el delegado pasa a una función especial (normalmente a través del operador – =) para que pueda eliminarse de la cadena del delegado (la cadena se camina hasta que se encuentra una referencia correspondiente, y ese enlace es eliminado de la cadena).

Vamos a crear una implementación de evento .NET simple pero no estándar. Aquí estoy usando la syntax de agregar / quitar menos común para exponer la cadena de delegates subyacente y permitirnos registrar la suscripción y la baja. Nuestro evento no estándar presenta un delegado con parámetros de un entero y una cadena en lugar del object sender normal del object sender y la subclase EventArgs :

 public delegate void BarHandler(int x, string y); public class Foo { private BarHandler delegateChain; public event BarHandler BarEvent { add { delegateChain += value; Console.WriteLine("Event handler added"); } remove { delegateChain -= value; Console.WriteLine("Event handler removed"); } } public void RaiseBar(int x, string y) { var temp = delegateChain; if(temp != null) { delegateChain(x, y); } } } 

Revisión de cómo funcionan las suscripciones Rx

Ahora considere cómo funcionan las transmisiones observables. Una suscripción a un observable se forma llamando al método Subscribe y pasando un objeto que implementa la IObserver , que tiene los OnNext , OnCompleted y OnError llamados por el observable para manejar eventos. Además, el método de Subscribe devuelve un identificador IDisposable que puede eliminarse.

Más típicamente, utilizamos métodos de extensión de conveniencia que sobrecargan Subscribe . Estas extensiones aceptan manejadores de delegates que se ajustan a las firmas OnXXX y crean transparentemente un AnonymousObservable cuyos métodos OnXXX invocarán a esos manejadores.

Puentear eventos .NET y Rx

Entonces, ¿cómo podemos crear un puente para extender los eventos .NET en las transmisiones observables de Rx? El resultado de llamar a Observable.FromEvent es crear un IObservable cuyo método de Subscribe actúa como una fábrica que creará este puente.

El patrón de evento .NET no tiene representación de eventos completados o de error. Solo de un evento que se plantea. En otras palabras, solo debemos relacionar tres aspectos del evento que se correlacionan con Rx de la siguiente manera:

  1. Suscripción, por ejemplo, una llamada a IObservable.Subscribe(SomeIObserver) asigna a fooInstance.BarEvent += barHandlerInstance .
  2. Invocación, por ejemplo, una llamada a barHandlerInstance(int x, string y) asigna a SomeObserver.OnNext(T arg)
  3. Anular la subscription , por ejemplo, asumiendo que conservamos el IDisposable devuelto de nuestra llamada Subscribe en una variable llamada subscription , luego una llamada a subscription.Dispose() . fooInstance.BarEvent -= barHandlerInstance subscription.Dispose() asigna a fooInstance.BarEvent -= barHandlerInstance .

Tenga en cuenta que solo es el acto de llamar a Subscribe que crea la suscripción. Por lo tanto, la llamada Observable.FromEvent está devolviendo una suscripción de soporte de fábrica, invocación y cancelación de suscripción del evento subyacente. En este punto, no hay ninguna suscripción de evento. Solo en el momento de llamar a Subscribe , el Observer estará disponible, junto con su controlador OnNext . Por lo tanto, la llamada FromEvent debe aceptar métodos de fábrica que pueda usar para implementar las tres acciones de puente en el momento apropiado.

Los argumentos del tipo FromEvent

Entonces, consideremos una implementación correcta de FromEvent para el evento anterior.

Recuerde que los controladores OnNext solo aceptan un único argumento. Los controladores de eventos .NET pueden tener cualquier cantidad de parámetros. Por lo tanto, nuestra primera decisión es seleccionar un solo tipo para representar las invocaciones de eventos en el flujo observable del objective.

De hecho, este puede ser cualquier tipo que desee que aparezca en su flujo observable objective. El trabajo de la función de conversión (discutido en breve) es proporcionar la lógica para convertir la invocación de evento en una invocación OnNext, y hay mucha libertad para decidir cómo sucede esto.

Aquí asignaremos los argumentos int x, string y de una invocación de BarEvent en una cadena formateada que describa ambos valores. En otras palabras, provocaremos una llamada a fooInstance.RaiseBar(1, "a") para dar como resultado una invocación de someObserver.OnNext("X:1 Y:a") .

Este ejemplo debería dejar de lado una fuente muy común de confusión: ¿qué representan los parámetros de tipo de FromEvent ? Aquí el primer tipo BarHandler es el tipo de delegado de evento .NET de origen , el segundo tipo es el tipo de argumento de controlador de OnNext destino. Debido a que este segundo tipo es a menudo una subclase EventArgs a menudo se supone que debe ser una parte necesaria del delegado del evento .NET: muchas personas OnNext alto el hecho de que su relevancia se debe realmente al controlador OnNext . Entonces, la primera parte de nuestra llamada a FromEvent ve así:

  var observableBar = Observable.FromEvent( 

La función de conversión

Ahora consideremos el primer argumento para FromEvent , la llamada función de conversión. (Tenga en cuenta que algunas sobrecargas de FromEvent omiten la función de conversión; más sobre esto más adelante).

La syntax lambda se puede truncar un poco gracias a la inferencia de tipo, así que aquí hay una versión de larga duración para comenzar:

 (Action onNextHandler) => { BarHandler barHandler = (int x, string y) => { onNextHandler("X:" + x + " Y:" + y); }; return barHandler; } 

Entonces, esta función de conversión es una función de fábrica que cuando se invoca crea un controlador compatible con el evento .NET subyacente. La función de fábrica acepta un delegado OnNext . Este delegado debe ser invocado por el manejador devuelto en respuesta a la función del manejador que se invoca con los argumentos de evento .NET subyacentes. El delegado se invocará con el resultado de convertir los argumentos del evento .NET a una instancia del tipo de parámetro OnNext . Por lo tanto, a partir del ejemplo anterior, podemos ver que se llamará a la función de fábrica con un onNextHandler de tipo Action ; debe invocarse con un valor de cadena en respuesta a cada invocación de evento .NET. La función de fábrica crea un controlador de delegado de tipo BarHandler para el evento .NET que maneja las invocaciones de eventos invocando onNextHandler con una cadena formateada creada a partir de los argumentos de la invocación de evento correspondiente.

Con un poco de inferencia de tipo, podemos colapsar el código anterior al siguiente código equivalente:

 onNextHandler => (int x, string y) => onNextHandler("X:" + x + " Y:" + y) 

Por lo tanto, la función de conversión cumple parte de la lógica de Suscripción de eventos al proporcionar una función para crear un controlador de eventos apropiado, y también hace el trabajo de asignar la invocación de eventos .NET a la invocación de manejador Rx OnNext .

Como se mencionó anteriormente, hay sobrecargas de FromEvent que omiten la función de conversión. Esto se debe a que no es necesario si el delegado del evento ya es compatible con la firma del método requerida para OnNext .

Los controladores de agregar / eliminar

Los dos argumentos restantes son addHandler y removeHandler, que son los responsables de suscribir y cancelar la suscripción del controlador delegado creado al evento .NET real. Suponiendo que tengamos una instancia de Foo llamada foo , la llamada FromEvent finalizada se FromEvent así:

 var observableBar = Observable.FromEvent( onNextHandler => (int x, string y) => onNextHandler("X:" + x + " Y:" + y), h => foo.BarEvent += h, h => foo.BarEvent -= h); 

Depende de nosotros decidir cómo se consigue el evento que vamos a salvar, por lo que proporcionamos las funciones de administrador de agregar y eliminar que esperan recibir el controlador de conversión creado. El evento generalmente se captura a través de un cierre, como en el ejemplo anterior donde cerramos sobre una instancia de foo .

Ahora tenemos todas las piezas para que FromEvent observable implemente por completo la suscripción, invocación y desuscripción.

Solo una cosa más…

Hay una última pieza de pegamento para mencionar. Rx optimiza las suscripciones al evento .NET. En realidad, para cualquier número dado de suscriptores a lo observable, solo se realiza una única suscripción al evento .NET subyacente. Esto es multidifusión a los suscriptores de Rx a través del mecanismo Publish . Es como si se hubiera agregado un Publish().RefCount() a lo observable.

Considere el siguiente ejemplo utilizando el delegado y la clase definidos anteriormente:

 public static void Main() { var foo = new Foo(); var observableBar = Observable.FromEvent( onNextHandler => (int x, string y) => onNextHandler("X:" + x + " Y:" + y), h => foo.BarEvent += h, h => foo.BarEvent -= h); var xs = observableBar.Subscribe(x => Console.WriteLine("xs: " + x)); foo.RaiseBar(1, "First"); var ys = observableBar.Subscribe(x => Console.WriteLine("ys: " + x)); foo.RaiseBar(1, "Second"); xs.Dispose(); foo.RaiseBar(1, "Third"); ys.Dispose(); } 

Esto produce el siguiente resultado, demostrando que solo se hace una suscripción:

 Event handler added xs: X:1 Y:First xs: X:1 Y:Second ys: X:1 Y:Second ys: X:1 Y:Third Event handler removed 

¡Ayudo a que esto ayude a despejar cualquier confusión persistente sobre cómo funciona esta compleja función!