Manejando eventos y mejorando la usabilidad con Reactive Extensions
Probablemente te has encontrado el siguiente problema más de una vez: digamos que tienes una caja de texto, y quieres mostrar sugerencias con respecto a lo que el usuario está ingresando, al momento en el que lo escribe. Lo más probable es que hayas usado el evento KeyDown para este fin, con algo de esta forma, que consume un webservice:
txtOrigen.KeyDown += async (sender, args) => { await App.ViewModel.Buscar(txtOrigen.Text); };
Esto funciona muy bien para uno, dos o tres caracteres. Pero si el texto es más largo, y el usuario escribe rápido, vas a tener una sucesión de eventos KeyDown, y cada uno de éstos llamará a tu webservice. Nada te asegura que las respuestas de tu webservice lleguen en secuencia, por lo que finalmente puedes estar mostrando las sugerencias de una consulta anterior, lo que es una pésima experiencia de usuario.
Este mismo problema te puede ocurrir, por ejemplo, al moverte por un mapa, si estás haciendo una consulta a un backend que te trae los pins u objetos que quieres mostrar en el mapa. Evidentemente, no te interesa mostrar los resultados de las consultas anteriores, te interesa mostrar el de la última consulta, lo que está mirando el usuario *ahora*.
Para solucionar este problema muy común, te sugiero usar Reactive Extensions. Esta librería te permite solucionar este y muchos otros problemas. El concepto básico es que tendrás que modificar un poco tu código para utilizar CancellationToken, un objeto que envías al método que realiza las consultas, para notificarle cuándo quieres cancelar una consulta y descartar las respuestas obtenidas. Recuerda que no te interesan las respuestas salvo la última.
Primero instala Reactive Extensions desde NuGet (instalar Rx-Main instala las otras dependencias):
Luego, reemplaza la asignación de KeyDown por lo siguiente, en el constructor de tu página (esto es para TextBox de Windows 8):
var queryTextChangedObservable = Observable.FromEventPattern<KeyEventHandler, KeyRoutedEventArgs> (s => txtOrigen.KeyDown += s, s => txtOrigen.KeyDown -= s); queryTextChangedObservable .Throttle(TimeSpan.FromMilliseconds(750)) .Scan(new { cts = new CancellationTokenSource(), e = default(EventPattern<KeyRoutedEventArgs>) }, (previous, newObj) => { previous.cts.Cancel(); return new { cts = new CancellationTokenSource(), e = newObj }; }) .ObserveOn(SynchronizationContext.Current) .Subscribe(async s => { var textBox = (TextBox)s.e.Sender; App.ViewModel.Buscar(textBox.Text, s.cts.Token); });
Se lee un poco complicado pero en el fondo no lo es. Las novedades son las siguientes:
- El evento a capturar es KeyDown, que es de tipo KeyEventHandler, y tiene como parámetro KeyRoutedEventArgs.
- Después de 750 milisegundos de que el evento deje de gatillarse, se ejecutará el código que aparece en el método Subscribe.
- Tenemos a nuestra disposición un objeto CancellationTokenSource (s.cts), con el cual podemos obtener el Token que pasaremos a nuestro método de consulta al backend.
- Los eventos anteriores que se estén ejecutando se cancelarán usando el código del método Scan.
Obviamente todo depende del tipo de evento y los parámetros que quieras usar. Personalmente lo estoy usando en TransantiagoMaster para los eventos ViewChangeEnded en Windows8, ViewChangeEnd en Windows Phone 7 y CenterChanged en Windows Phone 8.