Rx V – Schedulers y Linq2Events

Hoy os vamos a proponer el último artículo teórico acerca de las reactive extensions. Una vez hemos definido las rx, sabemos qué son los sujetos y las operaciónes de creación y Linq, ya podemos hablar de el último parámetro de la fórmula que definimos en su día: los Schedulers. Rx Schedulers

Es muy normal en el contexto actual de aplicaciones hablar de hilos de ejecución (Thread), de las Task Parallel Library o de sincronismo/asincronismo. Y es aquí donde vamos a encontrar la utilidad de los schedulers de las reactive extensions:

Schedulers

La traducción al castellano de esta palabra inglesa podría significar “programadores” o “planificadores”. Su función en este contexto es decidir dónde se van a ejecutar las operaciones. En Rx diferenciamos dos tipos de operaciones:

Para entender la diferencia entre estas dos operaciones vamos a crear una sentencia que nos lea cada una de las líneas de un fichero:

var filePath = "c:\mifichero.txt";
var observableLines = Observable.Create<string>(observer =>
{
    using (var reader = new StreamReader(filePath))
    {
        while (!reader.EndOfStream)
        {
            string line = reader.ReadLine();
            observer.OnNext(line);
        }
    }

    observer.OnCompleted();

     return () => { };
});

Como podemos ver, hemos creado un IObservable de string, en el que abriremos un StreamReader que apunta a un fichero de nuestro ordenador. Entonces empezamos a leer línea a línea y enviamos notivicaciones mediante un sujeto. Si ahora quisieramos suscribirnos a este observable, para que nos escriba en pantalla todas las líneas:

observableLines.Subscribe(line => Console.WriteLine);

Encontraremos que al ejecutar esta línea de código el programa se queda congelado en ella hasta que no termina de leer todo el archivo. Esto se debe a que estamos leyendo de forma síncrona en el momento de suscribirnos. Para solucionarlo tendríamos que ejecutar esta operación en otro contexto como por ejemplo un nuevo thread (hilo de ejecución):

observableLines
   .SubscribeOn(Scheduler.NewThread)
   .Subscribe(line => Console.WriteLine);

Al ejecutar este código, nos daremos cuenta de que la operación de leer el archivo se realiza en un nuevo hilo y esto hace que nuestro programa no se quede a la espera de que se termine de leerse hasta el final. Pero si estuvieramos en un programa WPF y en lugar de quererlo escribir por consola lo añadieramos a un control tipo TextBox, nos encontraríamos con un nuevo problema.

Cuando ejecutamos una aplicación tipo WPF o WinForms, las ventanas y controles son dibujadas por un hilo especial que se encarga de todo lo gráfico. La consecuencia de esto es que no se puede modificar un control gráfico desde otro hilo diferente a ese. Por lo que si ejecutamos este código:

observableLines
   .SubscribeOn(Scheduler.NewThread)
   .Subscribe(line => txtContent.Text += line);

En cuanto se lea (desde el nuevo hilo) la primera línea y la intente añadir a la propiedad Text de nuestro control de Texto, nos saltará una excepción debido a que es una operación invalida. Este momento en el que se evalúa cada una de las iteraciones es lo que antes hemos definido como “cuando observamos”. Para ello podríamos indicarle a nuestra sentencia que la operación de observar se ejecute en el contexto del hilo que trabaja con los gráficos. En WPF a este hilo se le conoce como el Dispatcher:

observableLines
   .ObserveOnDispatcher()
   .SubscribeOn(Scheduler.NewThread)
   .Subscribe(line => txtContent.Text += line);

Así podremos conseguir que nuestro código ejecute de forma asíncrona la lectura del fichero, pero que añada cada una de las líneas usando el hilo de ejecución de los gráficos.

Pero no solo existen dos contextos para poder planificar las operaciones de rx. Encontraremos (en dependencia de la plataforma):

 

Gracias a todos estos schedulers, tendremos un control total de nuestro programa, terminamos de explicar la fórmula de las reactive extensions y por tanto cerramos la explicación general de esta framework. Pero prácticamente todos los ejemplos que hemos visto hasta ahora van relacionados con lógica interna o llamadas asincronas y servicios.

Así que ahora vamos a tratar la última característica de las reactive extensions: su interacción con eventos.

Linq To Events

La primera vez que nos referimos a los sujetos, hablamos de sus semejanzas con la gestión de eventos. Podríamos compararlos de la siguiente forma:

rx vs. events

Ambos tienen su declaración, su suscripción, publicación y la forma de dejar de “escuchar” los acontecimientos. Y es gracias a esta característica que las reactive extensions nos proporcionarán una forma de gestionar los eventos con linq.

Dentro de las extensiones de rx encontramos dos específicas para crear observables a partir de eventos:

Una vez podemos observar un evento, obtendremos una gran facilidad para gestionarlo mediante sentencias linq. Característica que cuando exponemos mediante un ejemplo queda muy clara.

Buscando un tipo de aplicación que se base en eventos, nos hemos encontrado con un programa de dibujo, en el que pulsando con el ratón podemos dibujar lo que queramos. Así que vamos a desarrollar un pequeño proyecto a tal fin.

Nos hemos decidido por usar WPF como plataforma y el objetivo es crear un programa de dibujo en el que mientras pulsemos con el ratón sobre él, se irá dibujando en rojo la traza de nuestros movimientos de ratón. Por lo que el primer paso que tendremos que dar es crear un nuevo proyecto WPF en nuestro Visual Studio.

Automáticamente nos va a generar una ventana de inicio (MainWindow) sobre la que podremos trabajar. En WPF el control gráfico que nos deja dibujar se denomina Canvas (como en HTML5). Así pues, abriremos el archivo MainWindow.xaml y añadiremos un control de este tipo:

<Window x:Class="ReactiveDraw.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Canvas Loaded="Canvas_Loaded" Background="White" />
    </Grid>
</Window>

A nuestro control le hemos capturado el evento “Loaded” para que una vez esté cargado en pantalla se ejecute la función “Canvas_Loaded” en nuestro code-behind. Es por eso que el siguiente paso será trabajar en esta función. Si abrimos el archivo “MainWindow.cs” podremos empezar a programar.

Lo primero que deberíamos hacer en nuestra función es especificar el control con el que trabajamos:

private void Canvas_Loaded(object sender, RoutedEventArgs e)
{
    var canvas = (Canvas)sender;
}

Ahora crearemos eventos observables de nuestro ratón, según se pulse el botón, se deje de pulsar o se mueva por pantalla:

private void Canvas_Loaded(object sender, RoutedEventArgs e)
{
    var canvas = (Canvas)sender;

    var mouseDown = Observable
                        .FromEventPattern<MouseEventArgs>(canvas, "MouseLeftButtonDown");

    var mouseUp = Observable
                        .FromEventPattern<MouseEventArgs>(canvas, "MouseLeftButtonUp");

    var mouseMove = Observable
                        .FromEventPattern<MouseEventArgs>(canvas, "MouseMove")
                        .Select(ev => ev.EventArgs.GetPosition(this));
}

Como podemos ver capturaremos el evento de presionar el botón izquierdo del ratón (MouseLeftButtonDown), el de cuando lo levantamos (MouseLeftButtonUp) y para cuando movemos el ratón (MouseMove), recogeremos la posición del mismo.

Ahora tendríamos que decirle a nuestro programa que desde que se pulsa el botón, hasta que se deja de pulsar, recoga el movimiento del ratón:

var drawingPoints = from start in mouseDown
                        from move in mouseMove.TakeUntil(mouseUp)
                        select new Point
                                    {
                                        X = move.X,
                                        Y = move.Y
                                    };

Creo que la sentencia habla por si sola…

Por último solo tendríamos que suscribirnos al observable resultante de la sentencia y hacer que dibuje por ejemplo un punto en pantalla:

drawingPoints
    .Subscribe(point =>
                    {
                       var ellipse = new Ellipse
                                         {
                                             Stroke = Brushes.Red,
                                             StrokeThickness = 5
                                         };
                       canvas.Children.Add(ellipse);
                       Canvas.SetLeft(ellipse, point.X);
                       Canvas.SetTop(ellipse, point.Y);
                    });

Lo que hacemos es crear un círculo rojo de un tamaño de 5 puntos, lo añadimos a nuestro canvas y al final lo posicionamos.

Al ejecutar nuestro programa veremos que nos dibuja correctamente, pero que si movemos muy rápido el ratón, nos deja espacios. Para solucionar esto, lo que vamos a dibujar en realidad son líneas, de forma que si movemos muy rápido el puntero, tendremos almacenado el último punto y el nuevo, dibujando una recta que los una.

Para esto vamos a crear dos variables:

var isTheFirst = true; // indica si es el primer punto de la traza
var lastPosition = new Point(); // almacena el punto anterior de la traza

Con el fin de poder gestionar diferentes trazas (y no una línea continua), cada vez que levantemos el bottón izquierdo del ratón resetearemos el valor de “isTheFirst”:

var mouseUp = Observable
                        .FromEventPattern<MouseEventArgs>(canvas, "MouseLeftButtonUp")
                        .Do(_ => isTheFirst = true);

Y modificaremos la suscripción del evento de tal manera que dibujemos líneas basandonos en los datos que almacenamos. El código resultante de la función sería:

private void Canvas_Loaded(object sender, RoutedEventArgs e)
{
    var canvas = (Canvas)sender;
    var isTheFirst = true;
    var lastPosition = new Point();

    var mouseDown = Observable
                        .FromEventPattern<MouseEventArgs>(canvas, "MouseLeftButtonDown");

    var mouseUp = Observable
                        .FromEventPattern<MouseEventArgs>(canvas, "MouseLeftButtonUp")
                        .Do(_ => isTheFirst = true);

    var mouseMove = Observable
                        .FromEventPattern<MouseEventArgs>(canvas, "MouseMove")
                        .Select(ev => ev.EventArgs.GetPosition(this));

    var drawingPoints = from start in mouseDown
                        from move in mouseMove.TakeUntil(mouseUp)
                        select new Point
                                    {
                                        X = move.X,
                                        Y = move.Y
                                    };

    drawingPoints
        .Subscribe(point =>
                       {
                           var ellipse = new Ellipse
                                             {
                                                 Stroke = Brushes.Red,
                                                 StrokeThickness = 5
                                             };
                           canvas.Children.Add(ellipse);
                           Canvas.SetLeft(ellipse, point.X);
                           Canvas.SetTop(ellipse, point.Y);
                                if (isTheFirst)
                                {
                                    isTheFirst = false;
                                    lastPosition = point;
                                    return;
                                }

                                canvas.Children.Add(new Line
                                                        {
                                                            Stroke = Brushes.Red,
                                                            X1 = lastPosition.X,
                                                            X2 = point.X,
                                                            Y1 = lastPosition.Y,
                                                            Y2 = point.Y
                                                        });

                                lastPosition = point;
                            });
}

Si volvemos a ejecutar nuestro programa de dibujo, veremos que con un código muy cercano al lenguaje corriente, hemos realizado una operación que antes de conocer las reactive extensions hubiera sido un verdadero lío de funciones, variables y demás.

 

Conclusiones

Hasta este punto hemos podido explicaros casi todas las características de las reactive extensions. Desde en qué se basan, pasando por sus funciones, hasta cómo usarlas en diferentes contextos.

El resumen de toda la información expuesta en estos últimos meses, es que las reactive extensions son una herramienta muy potente para el desarrollo. Se basa en un paradigma novedoso y útil. Y su función es facilitar las operaciones que ya realizábamos usando un lenguaje más cercano al que usamos para comunicarnos unos con otros.

En unos días y en referencia a este tema, publicaremos el último artículo en el que hablaremos de la codemotion, haremos un resumen de todo lo que contamos allí, crearemos un índice de los artículos y además añadiremos mucho código fuente con ejemplos de todo tipo para que podáis ver las reactive extensions en un ámbito de ejecución.

Permaneced atentos :)