Tecnología de vanguardia

Una barra de progreso contextual para ASP.NET MVC

Dino Esposito

Descargar el ejemplo de código

Dino EspositoLa mayoría de los usuarios de una aplicación informática desean recibir comentarios apropiados de esta siempre que comienza una operación potencialmente larga. Implementar este patrón en una aplicación de Windows no es gran cosa, aunque tampoco es tan sencillo como se podría esperar. Implementar el mismo patrón en la Web genera algunas dificultades adicionales, las cuales se relacionan en su mayoría con la naturaleza inherentemente sin estado de la Web.

Sobre todo en el caso de los desarrolladores web, mostrar algunos comentarios estáticos al principio de la operación es fácil. De manera que el enfoque de esta columna no es sobre cómo mostrar una simple cadena de texto que dice "Espere" o una imagen animada. En su lugar, esta columna aborda el problema de informar el estado de operaciones remotas con comentarios contextualizados que representan fielmente el estado de la operación para una sesión determinada. En el caso de aplicaciones web, las tareas largas ocurren en el servidor y no existen instalaciones integradas que traspasen la información de estado al explorador cliente. Para lograr esto, debe crear su propio marco con una mezcla de código de servidor y cliente. Las bibliotecas como jQuery son útiles, pero no proporcionan una solución integrada, ni siquiera en la amplia selección de complementos jQuery.

En esta columna (la primera de una breve serie), analizaré el patrón AJAX más común que encontrará detrás de cualquier marco de progreso contextual y crearé una solución personalizada para aplicaciones ASP.NET MVC. Si usted es un desarrollador de formularios Web Forms, le conviene dar un vistazo a un par de columnas que escribí el verano de 2007 que abordaban justamente los formularios Web Forms y AJAX (consulte mi lista de columnas en bit.ly/psNZAc).

El patrón del "indicador de progreso"

El problema fundamental con un informe de progreso basado en AJAX es proporcionar comentarios sobre el estado de la operación mientras el usuario espera una respuesta del servidor. Dicho de otra manera: El usuario inicia una operación AJAX que demora un poco en finalizar; mientras tanto, recibe actualizaciones sobre el progreso realizado. El problema estructural es que el usuario no va a recibir respuestas parciales; la operación devuelve su respuesta solo cuando todo el trabajo del servidor ha finalizado. Para devolver respuestas parciales al cliente, se debe establecer cierto tipo de sincronización entre el cliente AJAX y la aplicación del servidor. El patrón de "indicador de progreso" aborda este punto.

Este patrón sugiere que estructure operaciones de servidor ad hoc que escriban información sobre su estado en una ubicación conocida. El estado se sobrescribe cada vez que la operación realiza una cantidad significativa de progreso. Al mismo tiempo, el cliente abre un segundo canal y realiza lecturas periódicas desde la misma ubicación conocida. De esta manera, cualquier cambio se detecta con prontitud y se informa al cliente. Lo que es más importante, los comentarios son contextuales y reales. Representa un progreso eficaz en el servidor y no se basa en suposiciones ni cálculos.

La implementación del patrón requiere que escriba sus operaciones de servidor para que estas estén conscientes de sus roles. Por ejemplo, supongamos que implementa una operación de servidor basada en un flujo de trabajo. Antes de comenzar cada paso significativo del flujo de trabajo, la operación invocará algún código que actualice una tienda durable con algo de información relativa a la tarea. La tienda durable puede ser una tabla de base de datos o una memoria compartida. La información relativa a una tarea puede ser un número que indica el porcentaje de trabajo realizado o un mensaje que describe la tarea en curso.

Por otro lado, necesita un servicio basado en JavaScript que al mismo tiempo lea el texto de la tienda durable y lo devuelva al cliente. Por último, el cliente usará el código JavaScript para fusionar el texto con la UI existente. Esto puede originar que cierto texto simple aparezca en una etiqueta DIV o en algo más sofisticado como una barra de progreso basada en HTML.

Indicador de progreso en ASP.NET MVC

Veamos qué se necesita para implementar el patrón de indicador de progreso en ASP.NET MVC. La operación del servidor es esencialmente un método de acción del controlador. El controlador puede ser sincrónico o asincrónico. La asincronía en los controladores es buena solo para el estado y la capacidad de respuesta de la aplicación del servidor; no tiene ningún efecto en el tiempo que el usuario tiene que esperar por una respuesta. El patrón de indicador de progreso funciona bien con cualquier tipo de controlador.

Para invocar y luego supervisar una operación de servidor, necesita un poco de AJAX. Sin embargo, una biblioteca AJAX no debe limitarse a realizar una solicitud y esperar la respuesta. La biblioteca también debe ser capaz de configurar un temporizador que envíe periódicamente una llamada a algún extremo que devuelva el estado actual de la operación. Por este motivo, jQuery o el objeto nativo XMLHttpRequest son necesarios, pero no suficientes. De manera que creé un objeto JavaScript sencillo para ocultar la mayoría de los pasos adicionales que se requieren con una llamada AJAX supervisada. Desde una perspectiva ASP.NET MVC, se invoca la operación a través del objeto JavaScript.

El método del controlador responsable para la operación usará la porción del lado del servidor del marco para almacenar el estado actual en una ubicación conocida (y compartida), como la memoria caché ASP.NET. Finalmente, el controlador debe exponer un extremo común para que las solicitudes periódicas realicen una llamada a fin de leer el estado en tiempo real. Veamos primero el marco completo en acción y luego exploremos los aspectos internos.

Introducción al marco SimpleProgress

Escrito específicamente para este artículo, el marco SimpleProgress (SPF) consta de un archivo JavaScript y una biblioteca de clases. La biblioteca de clases define una clase clave (la clase ProgressManager) que gobierna la ejecución de la tarea y cualquier actividad de supervisión. En la Figura 1 se muestra un método de acción de controlador de ejemplo que usa el marco. Observe que este código (potencialmente de larga ejecución) en realidad debería ir en un controlador asincrónico para evitar bloquear un subproceso ASP.NET demasiado tiempo.

Figura 1 Método de acción de controlador que usa el marco SimpleProgress

public String BookFlight(String from, String to)
{
  var taskId = GetTaskId();
  // Book first flight
  ProgressManager.SetCompleted(taskId,
    String.Format("Booking flight: {0}-{1} ...", from, to));
  Thread.Sleep(2000);
  // Book return flight
  ProgressManager.SetCompleted(taskId,
    String.Format("Booking flight: {0}-{1} ...", to, from));
  Thread.Sleep(1000);
  // Book return
  ProgressManager.SetCompleted(taskId,
    String.Format("Paying for the flight ..."));
  Thread.Sleep(2000);
  // Some return value
  return String.Format("Flight booked successfully");
}

Como puede ver, la operación se basa en tres pasos principales. Por simplicidad, la acción real se ha omitido y la reemplaza una llamada Thread.Sleep. Más importante, puede ver tres llamadas a SetCompleted que en realidad escriben el estado actual del método en una ubicación compartida. Los detalles de la ubicación se encierran en la clase ProgressManager. En la Figura 2 se muestra lo necesario para invocar y supervisar un método de controlador.

Figura 2 Invocación y supervisión de un método a través de JavaScript

<script type="text/javascript">
  $(document).ready(function () {
    $("#buttonStart").bind("click", buttonStartHandler);
  });
  function buttonStartHandler() {
    new SimpleProgress()
    .setInterval(600)
    .callback(
      function (status) { $("#progressbar2").text(status); },
      function (response) { $("#progressbar2").text(response); })
    .start("/task/bookflight?from=Rome&to=NewYork", "/task/progress");
  }
</script>

Observe que por cuestión de legibilidad, mantuve el botón StartHandler de la Figura 2 fuera del controlador listo del documento. Al hacerlo, sin embargo, agrego un poco de contaminación al alcance global de JavaScript al definir una nueva función globalmente visible.

Primero se establece un par de parámetros como la URL que se llamará de vuelta para recoger el estado actual y las llamadas que se invocarán para actualizar la barra de progreso y la UI una vez que la tarea larga haya finalizado. Por último, se inicia la tarea.

La clase del controlador debe incorporar algunas capacidades adicionales. Específicamente, debe exponer un método para que se llame de vuelta. Este código es relativamente estándar y lo codifiqué en una clase básica de la cual puede heredar su controlador, como se muestra aquí:

public class TaskController : SimpleProgressController
{
  ...
  public String BookFlight(String from, String to)
  {
    ...
  }
}

Toda la clase SimpleProgressController se muestra en la Figura 3.

Figura 3 Clase básica para controladores que incorporan acciones supervisadas

public class SimpleProgressController : Controller
{
  protected readonly ProgressManager ProgressManager;
  public SimpleProgressController()
  {
    ProgressManager = new ProgressManager();
  }
  public String GetTaskId()
  {
    // Get the header with the task ID
    var id = Request.Headers[ProgressManager.HeaderNameTaskId];
    return id ?? String.Empty;
  }
  public String Progress()
  {
    var taskId = GetTaskId();
    return ProgressManager.GetStatus(taskId);
  }
}

La clase tiene dos métodos. GetTaskId recupera la identificación única de la tarea que representa la clave para recuperar el estado de una llamada específica. Como verá en más detalle más adelante, la identificación de la tarea la genera el marco JavaScript y se envía con cada solicitud mediante un encabezado HTTP personalizado. El otro método que encuentra en la clase SimpleProgressController representa el extremo público (y común) que el marco JavaScript llamará de vuelta para obtener el estado de una instancia de tarea específica.

Antes de adentrarme en más detalles de la implementación, en la Figura 4 obtendrá una idea concreta de los resultados que SPF le permite lograr.

The Sample Application in Action
Figura 4 La aplicación de ejemplo en acción

La clase ProgressManager

La clase ProgressManager suministra la interfaz para leer y escribir el estado actual en una tienda compartida. La clase se basa en la siguiente interfaz:

public interface IProgressManager
{
  void SetCompleted(String taskId, Int32 percentage);
  void SetCompleted(String taskId, String step);
  String GetStatus(String taskId);
}

El método SetCompleted almacena el estado como un porcentaje o una cadena sencilla. El método GetStatus lee cualquier contenido de vuelta. La interfaz IProgressDataProvider abstrae la tienda compartida:

public interface IProgressDataProvider
{
  void Set(String taskId, String progress, Int32 durationInSeconds=300);
  String Get(String taskId);
}

La implementación actual del SPF entrega solo un proveedor de progreso que guarda su contenido en la memoria caché ASP.NET. La clave para identificar el estado de cada solicitud es la identificación de la tarea. En la Figura 5 se muestra un proveedor de datos de progreso de ejemplo.

Figura 5 Proveedor de datos de progreso basado en el objeto de la memoria caché ASP.NET

public class AspnetProgressProvider : IProgressDataProvider
{
  public void Set(String taskId, String progress, Int32 durationInSeconds=3)
  {
    HttpContext.Current.Cache.Insert(
      taskId,
      progress,
      null,
      DateTime.Now.AddSeconds(durationInSeconds),
      Cache.NoSlidingExpiration);
  }
  public String Get(String taskId)
  {
    var o = HttpContext.Current.Cache[taskId];
    if (o == null)
      return String.Empty;
    return (String) o;
  }
}

Tal como se mencionó, cada solicitud para una tarea supervisada se asocia con una identificación única. La identificación es un número aleatorio generado por el marco JavaScript y que pasa del cliente al servidor a través de un encabezado HTTP de la solicitud.

El marco JavaScript

Uno de los motivos por los cuales la biblioteca jQuery se volvió tan popular es la API de AJAX. La API de AJAX es eficaz y contiene muchas características y una lista de funciones abreviadas que facilitan enormemente hacer una llamada en AJAX. Sin embargo, la API nativa de AJAX no admite supervisión de progreso. Por esta razón, se necesita una API contenedora que use jQuery (o cualquier otro marco JavaScript que le guste) para realizar la llamada en AJAX mientras se asegura de que se genere una identificación de tarea aleatoria para cada llamada y se active el servicio de supervisión. En la Figura 6 se aprecia un extracto del archivo SimpleProgress-Fx.js en la descarga de código que acompaña que ilustra la lógica tras el inicio de una llamada remota.

Figura 6 Código de script para invocar el marco SimpleProgress

var SimpleProgress = function() {
  ...
  that.start = function (url, progressUrl) {
    that._taskId = that.createTaskId();
    that._progressUrl = progressUrl;
    // Place the AJAX call
    $.ajax({
      url: url,
      cache: false,
      headers: { 'X-SimpleProgress-TaskId': that._taskId },
      success: function (data) {
        if (that._taskCompletedCallback != null)
          that._taskCompletedCallback(data);
        that.end();
      }
    });
    // Start the callback (if any)
    if (that._userDefinedProgressCallback == null)
      return this;
    that._timerId = window.setTimeout(
      that._internalProgressCallback, that._interval);
  };
}

Una vez generada la identificación de la tarea, la función agrega la como un encabezado HTTP personalizado al autor de la llamada en AJAX. Justo después de activar la llamada en AJAX, la función configura un temporizador que invoca periódicamente una llamada de vuelta. Esta llamada de vuelta lee el estado actual y pasa el resultado a una función definida por el usuario para actualizar la UI.

Yo estoy usando jQuery para realizar la llamada en AJAX; en este respecto, es importante que desactive el almacenamiento en caché del explorador para llamadas en AJAX. En jQuery, el almacenamiento en caché está activado de manera predeterminada y se desactiva automáticamente para ciertos tipos de datos como script y JSON With Padding (JSONP).

No es una tarea fácil

Supervisar operaciones continuas no es una tarea fácil en aplicaciones web. Las soluciones basadas en sondeos son comunes pero no inevitables. Un proyecto GitHub interesante que implementa conexiones persistentes en ASP.NET es SignalR (github.com/signalr). Al usar SignalR, puede solucionar el mismo problema que se analiza aquí sin sondear si hay cambios.

En esta columna, analicé un marco de ejemplo (cliente y servidor) que intenta simplificar la implementación de una barra de progreso contextual. Mientras el código se optimiza para ASP.NET MVC, el patrón subyacente es absolutamente general y también se puede emplear en aplicaciones de formularios Web Forms de ASP.NET. Si descarga y experimenta con el código fuente, no dude en compartir sus pensamientos y comentarios.

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, Esposito participa habitualmente en conferencias y eventos del sector en todo el mundo. Puede seguir a Dino por Twitter en twitter.com/despos.

Gracias a los siguientes expertos técnicos por su ayuda en la revisión de esta columna: Damian Edwards, Phil Haack y Scott Hunter