Tecnología de vanguardia

Creación de una barra de progreso con SignalR

Dino Esposito

Descargar el ejemplo de código

Dino EspositoEn las últimas dos entregas de esta columna analicé cómo crear una solución con ASP.NET para el eterno problema de supervisar el progreso de una tarea remota del lado cliente en una aplicación web. A pesar del éxito y la adopción generalizada de AJAX todavía no existe una solución completa y ampliamente aceptada para mostrar una barra de progreso contextual dentro de una aplicación web sin recurrir a Silverlight o Flash.

Para ser sincero, no hay muchas formas para lograrlo. Si quiere, puede crear su propia solución, pero el patrón subyacente no será muy diferente de lo que yo presenté (dirigido en concreto a ASP.NET MVC) en los artículos anteriores. Este mes retomo el mismo asunto, pero analizaré cómo crear una barra de progreso con una nueva biblioteca que está en proceso de desarrollo: SignalR.

SignalR es una biblioteca para .NET Framework y un complemento para jQuery que está desarrollando el equipo de ASP.NET, y que posiblemente se incluirá en las versiones futuras de la plataforma ASP.NET. Presenta algunas funciones extremadamente prometedoras que actualmente no se encuentran en .NET Framework, y que más y más desarrolladores están exigiendo.

SignalR a primera vista

SignalR es una biblioteca integrada para cliente y servidor que permite que los clientes basados en el explorador y los componentes del servidor basados en ASP.NET puedan realizar conversaciones bidireccionales y en varios pasos. En otras palabras, la conversación no es un simple intercambio de datos de solicitud y respuesta sin estado; por el contrario, continúa hasta que se cierra en forma explícita. La conversación tiene lugar sobre una conexión persistente y permite que el cliente envíe varios mensajes al servidor, y que el servidor responda y (mucho más interesante aún) envíe mensajes asincrónicos al cliente.

No debiera sorprender a nadie que el ejemplo canónico que usaré para ilustrar las funciones principales de SignalR es una aplicación de chat. El cliente inicia la conversación al enviar un mensaje al servidor. El servidor, un extremo ASP.NET, responde y sigue a la espera de solicitudes nuevas.

SignalR está especializado para un contexto de web y requiere de jQuery 1.6 (o posterior) en el cliente y ASP.NET en el servidor. Puede instalar SignalR con NuGet o descargar las partes directamente del repositorio GitHub en github.com/SignalR/SignalR. En la Figura 1 aparece la página de NuGet con todos los paquetes de SignalR. Como mínimo, deberá descargar SignalR, que depende de SignalR.Server para la parte del servidor del marco, y SignalR.Js para la parte del cliente web del marco. Los otros paquetes que aparecen en la Figura 1 cumplen funciones más específicas, como por ejemplo proporcionar un cliente .NET, una resolución de dependencias Ninject y un mecanismo de transporte alternativo con HTML5 Web sockets.

SignalR Packages Available on the NuGet PlatformFigura 1 Paquetes disponibles en la plataforma NuGet para SignalR

Detalles del ejemplo de Chat

Antes de intentar crear una solución para la barra de progreso, sería útil que se familiarice con la biblioteca y eche una mirada al capítulo distribuido con el código fuente descargable (archive.msdn.microsoft.com/mag201203CuttingEdge) y el resto de la información a la que se hace referencia en los (pocos) artículos disponibles actualmente en la red sobre el tema. Pero recuerde que el proyecto SignalR no se ha publicado aún.

En el contexto de un proyecto ASP.NET MVC, comenzamos por hacer referencia a unos cuantos archivos de scripts, del siguiente modo:

    <script src="@Url.Content("~/Scripts/jquery-1.6.4.min.js")"
      type="text/javascript"></script>
    <script src="@Url.Content("~/Scripts/jquery.signalr.min.js")"
      type="text/javascript"></script>
    <script src="@Url.Content("~/signalr/hubs")"
      type="text/javascript"></script>

Observe, que ninguna parte de SignalR es específica para ASP.NET MVC; por lo tanto, la biblioteca también se puede usar perfectamente con aplicaciones escritas con Web Forms.

Un punto interesante que merece atención es que los dos primeros vínculos hacen referencia a un archivo de un script determinado. El tercer vínculo, sin embargo, también hace referencia a contenido JavaScript, pero ese contenido se genera sobre la marcha, y eso depende de otro código que se encuentra dentro de la aplicación host ASP.NET. Tenga en cuenta también que para ofrecer compatibilidad con versiones de Internet Explorer anteriores a la versión 8 necesita la biblioteca JSON2.

Cuando se carga la página, finalizamos la configuración del cliente y abrimos la conexión. En la Figura 2 vemos el código necesario. Lo más probable es que queramos llamar este código dentro del evento ready de jQuery. El código enlaza los controladores de script con los elementos de HTML (JavaScript discreto) y prepara la conversación con SignalR.

Figura 2 Configuración de la biblioteca SignalR para un programa de chat

    <script type="text/javascript">
      $(document).ready(function () {    // Add handler to Send button
        $("#sendButton").click(function () {
          chat.send($('#msg').val());
        });
        // Create a proxy for the server endpoint
        var chat = $.connection.chat; 
        // Add a client-side callback to process any data
        // received from the server
        chat.addMessage = function (message) {
          $('#messages').append('<li>' + message + '</li>');
        };
        // Start the conversation
        $.connection.hub.start();
      });
    </script>

Vale la pena mencionar que el objeto $.connection está definido en el archivo de script de SignalR. El objeto chat, en cambio, es un objeto dinámico en el sentido que su código se genera sobre la marcha y se inyecta en la página del cliente por medio de la referencia de script de Hub. El objeto chat es fundamentalmente un proxy en JavaScript para un objeto del lado servidor. A estas alturas debería quedar claro que el código cliente en la Figura 2 significa (y hace) muy poco sin una contraparte fuerte del lado servidor.

El proyecto ASP.NET debería incluir una referencia al ensamblado de SignalR y sus dependencias, por ejemplo a Microsoft.Web.Infrastructure. El código del lado servidor incluye una clase administrada que calza con el objeto de JavaScript que creamos. Con respecto al código en la Figura 2, necesitamos un objeto del lado servidor que tenga la misma interfaz que el objeto Chat del lado cliente. En el servidor esta clase se hereda de la clase Hub que está definida en el ensamblado de SignalR. La clase tiene la siguiente firma:

using System;
using SignalR.Hubs;
namespace SignalrProgressDemo.Progress
{
  public class Chat : Hub
  {
    public void Send(String message)
    {
      Clients.addMessage(message);
    }
  }
}

Todos los métodos públicos de la clase deben coincidir con un método JavaScript en el cliente. O como mínimo, todos los métodos invocados en el objeto JavaScript deben tener un método concordante en la clase del servidor. Por lo tanto, el método Send que invocamos en el script de la Figura 2 termina por realizar una llamada al método Send del objeto Chat que definimos previamente. Para enviar los datos de vuelta al cliente, el código del servidor emplea la propiedad Clients de la clase Hub. El miembro Clients es del tipo dinámico, lo que le permite hacer referencia en forma dinámica a ciertos objetos. En concreto, la propiedad Clients contiene una referencia a un objeto del lado servidor que se creó según la interfaz del objeto cliente: el objeto Chat. Como el objeto Chat en la Figura 2 cuenta con un método addMessage, el mismo método addMessage también debe estar expuesto en el objeto Chat del lado servidor.

Hacia una barra de progreso de ejemplo

Usemos ahora SignalR para crear un sistema de notificación que informe al cliente sobre cualquier progreso que realice el servidor en una tarea que pueda ser extensa. Como primer paso, vamos a crear una clase del lado servidor para encapsular la tarea. El nombre que daremos a esta clase, aunque sea arbitrario, influirá posteriormente en el código que escribamos más adelante. Esto significa simplemente que tenemos una razón más para elegir con cuidado el nombre de la clase. Y mas importante aún, esta clase se heredará de una clase proporcionada por SignalR llamada Hub. Esta es la firma:

public class BookingHub : Hub
{
  ...
}

La clase BookingHub contará con pocos métodos públicos; serán principalmente métodos void que aceptan cualquier secuencia de parámetros de entrada que tengan sentido para el fin previsto. Todos los métodos públicos en una clase del tipo Hub representan un extremo posible que podrá ser invocado por el cliente. Como ejemplo, agreguemos un método para reservar un pasaje aéreo:

public void BookFlight(String from, String to)
{
  ...
}

Este método debería contener toda la lógica necesaria para ejecutar esa acción específica (es decir, reservar un pasaje aéreo). En diferentes partes del código también habrá llamadas que de alguna forma van a informar el progreso de vuelta al cliente. Supongamos que este es el esqueleto del método BookFlight:

public void BookFlight(String from, String to)
{
  // Book first leg  var ref1 = BookFlight(from, to);  // Book return flight
  var ref2 = BookFlight(to, from);
  // Handle payment
  PayFlight(ref1, ref2);
}

Junto con estas dos operaciones principales, queremos notificar al usuario sobre el progreso. La clase base Hub ofrece una propiedad llamada Clients, que está definida como un tipo dinámico. En otras palabras, invocaremos un método de este objeto para devolver la llamada al cliente. La forma de este método, sin embargo, está determinada por el cliente mismo. Pasemos, por lo tanto, al cliente.

Tal como mencioné, en la página del cliente tendremos algún código de script que se ejecutará al cargar la página. Cuando usamos jQuery, el evento $(document).ready es un buen lugar para ejecutar este código. Primero, recibimos un proxy para el objeto server:

var bookingHub = $.connection.bookingHub;
// Some config work
...
// Open the connection
$.connection.hub.start();

El nombre del objeto al que hacemos referencia en el componente nativo $.connection SignalR es simplemente un proxy que se crea en forma dinámica y que expone la interfaz pública del objeto BookingHub para el cliente. El proxy se genera mediante el vínculo signalr/hubs que se encuentra en la sección <script> de la página. La convención de nomenclatura para los nombres es camelCase. Por lo tanto, la clase BookingHub en C# se convierte en el objeto bookingHub en JavaScript. En este objeto encontraremos los métodos que coinciden con la interfaz pública del objeto server. Además, en el caso de los métodos la convención de nomenclatura usa los mismos nombres, pero convertidos en camelCase. Podemos agregar un controlador para clics en un botón de HTML e iniciar una operación en el servidor mediante AJAX, como se observa en estas líneas:

bookingHub.bookFlight("fco", "jfk");

Ahora podemos definir los métodos en el cliente para controlar las respuestas. Por ejemplo, podemos definir un método displayMessage en el proxy del cliente que reciba un mensaje y lo muestre con una etiqueta span de HTML:

bookingHub.displayMessage = function (message) {
  $("#msg").html(message);
};

Observe que ahora nosotros somos los responsables de la firma del método displayMessage. Nosotros decidimos lo que se pasa y de qué tipo esperamos que sean los tipos de entrada.

Para cerrar el círculo, solo queda un tema pendiente: ¿quién llama a displayMessage y quién es el responsable final de pasar los datos? Esta responsabilidad recae sobre el código de Hub del lado cliente. El método displayMessage (y cualquier otro método de devolución de llamada del que queramos disponer) lo llamamos desde el objeto Hub mediante el objeto Clients. En la Figure 3 aparece la versión final de la clase Hub.

Figura 3 Versión final de la clase Hub

public void BookFlight(String from, String to)
{
  // Book first leg
  Clients.displayMessage(    String.Format("Booking flight: {0}-{1} ...", from, to));
  Thread.Sleep(2000);
  // Book return
  Clients.displayMessage(    String.Format("Booking flight: {0}-{1} ...", to, from));
  Thread.Sleep(3000);
  // Book return
  Clients.displayMessage(    String.Format("Booking flight: {0}-{1} ...", to, from));
  Thread.Sleep(2000);
  // Some return value
  Clients.displayMessage("Flight booked successfully.");
}

Observe que en este caso el nombre displayMessage debe coincidir perfectamente en el uso de las mayúsculas y minúsculas con el nombre que se encuentra en el código JavaScript. Cualquier equivocación, como por ejemplo DisplayMessage, no generará ninguna excepción, pero tampoco ninguna ejecución.

Como el código de Hub está implementado en un objeto Task, se ejecuta en un subproceso propio y no afecta al grupo de subprocesos de ASP.NET.

Si una tarea del servidor provoca que se programe algún trabajo asincrónico, elegirá un subproceso del grupo de trabajo estándar. La ventaja es que los controladores de solicitud de SignalR son asincrónicos, lo que significa que mientras se encuentran en el estado de espera, esperando mensajes nuevos, no usan ningún subproceso. Cuando reciben un mensaje y tienen que realizar alguna labor, entonces se usa un subproceso de trabajo de ASP.NET.

Una barra de progreso verdadera con HTML

En este artículo y los anteriores usé frecuentemente el término barra de progreso sin mostrar ni una sola vez una barra indicadora clásica como ejemplo de la interfaz de usuario en el cliente. Una barra indicadora no es más que un efecto visual agradable y no requiere de ningún código complejo adicional en la infraestructura asincrónica. Sin embargo, en la Figura 4 vemos el código en JavaScript que crea una barra indicadora sobre la marcha, dado algún valor de porcentaje. La apariencia de los elementos HTML se puede cambiar mediante las clases CSS pertinentes.

Figura 4 Creación de una barra indicadora con HTML

var GaugeBar = GaugeBar || {};
GaugeBar.generate = function (percentage) {
  if (typeof (percentage) != "number")
    return;
  if (percentage > 100 || percentage < 0)
    return;
  var colspan = 1;
  var markup = "<table class='gauge-bar-table'><tr>" +
    "<td style='width:" + percentage.toString() +
    "%' class='gauge-bar-completed'></td>";
  if (percentage < 100) {
    markup += "<td class='gauge-bar-tobedone' style='width:" +
      (100 - percentage).toString() +
      "%'></td>";
    colspan++;
  }
  markup += "</tr><tr class='gauge-bar-statusline'><td colspan='" +
    colspan.toString() +
    "'>" +
    percentage.toString() +
    "% completed</td></tr></table>";
  return markup;
}

Este método se llama desde el controlador de clics de un botón:

bookingHub.updateGaugeBar = function (perc) {
  $("#bar").html(GaugeBar.generate(perc));
};

Por lo tanto, el método updateGaugeBar se invoca desde otro método de Hub que simplemente usa una devolución de llamada diferente para informar sobre el progreso. Podemos reemplazar el método displayMessage que usamos recién con updateGaugeBar dentro de un método de Hub.

No solo clientes web

Presenté SignalR esencialmente como una API que requiere de un front end en la web. Aunque probablemente esto es la situación más común para usarlo, SignalR no se limita exclusivamente al dominio de los clientes web. Puede descargar un cliente para aplicaciones .NET de escritorio y pronto se publicará otro cliente para Windows Phone.

En este artículo no entregué más que una pincelada somera sobre SignalR, en el sentido que presenté la forma más sencilla y eficaz para su programación. En una columna posterior investigaré algunas de las cosas mágicas que ocurren tras el telón y la forma en que se transmiten los paquetes. Permanezca atento a las novedades.

Dino Esposito es el autor del libro “Programming Microsoft ASP.NET MVC3” (Microsoft Press, 2011) y coautor de “Microsoft .NET: Architecting Applications for the Enterprise” (Microsoft Press, 2008). Con residencia en Italia, participa habitualmente en conferencias y eventos del sector en todo el mundo. Puede seguir a Dino por Twitter en twitter.com/despos.

Gracias al siguiente experto técnico por su ayuda en la revisión de este artículo: Damian Edwards