.net - Cómo utilizar Observable.FromEvent en lugar de FromEventPattern y evitar nombres de eventos literales de cadena
winforms system.reactive (1)
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<KeyPressEventArgs>(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<KeyPressEventHandler, KeyPressEventArgs>(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 resto, 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 objetivo: 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<KeyPressEventHandler, KeyPressEventArgs>(
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<KeyPressEventHandler, KeyPressEventArgs>(
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<T>
) 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 delegados. Los delegados 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 delegados (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 sintaxis de agregar / quitar menos común para exponer la cadena de delegados 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<T>
, 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 delegados que se ajustan a las firmas OnXXX
y crean transparentemente un AnonymousObservable<T>
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:
- Suscripción, por ejemplo, una llamada a
IObservable<T>.Subscribe(SomeIObserver<T>)
asigna afooInstance.BarEvent += barHandlerInstance
. - Invocación, por ejemplo, una llamada a
barHandlerInstance(int x, string y)
asigna aSomeObserver.OnNext(T arg)
- Anular la
subscription
, por ejemplo, asumiendo que conservamos elIDisposable
devuelto de nuestra llamadaSubscribe
en una variable llamadasubscription
, luego una llamada asubscription.Dispose()
.fooInstance.BarEvent -= barHandlerInstance
subscription.Dispose()
asigna afooInstance.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 objetivo.
De hecho, este puede ser cualquier tipo que desee que aparezca en su flujo observable objetivo. 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<BarHandler, string>(
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 sintaxis 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<string> 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<string>
; 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<BarHandler, string>(
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<BarHandler, string>(
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!