Este artigo foi traduzido por máquina.

The Cutting Edge

Melhorando a barra de progresso contextual para o ASP.NET MVC

Dino Esposito

Dino Esposito
No mundo da Web, o termo "barra de progresso" significa muitas coisas diferentes para pessoas diferentes. Às vezes refere-se ao texto estático que simplesmente mostra que alguma operação está a coloca em algum lugar. O texto fornece um feedback básica para o usuário e essencialmente convida para apenas relaxar e esperar. Às vezes, a barra de progresso exibe uma animação simple ao enviar a mesma mensagem para o usuário — aguarde — que pelo menos se concentra a atenção do usuário porque os usuários tendem a seguir os elementos móveis. Uma típica animação mostra um elemento gráfico que se move em torno de um caminho linear ou circular indefinidamente. Quando o elemento móvel chega ao fim do caminho, a animação recomeça e continua em execução até que termina a operação subjacente e a animação é interrompida por meio de programação. Este é um padrão bastante comum em, por exemplo, a maioria das companhias aéreas sites da Web.

No mês passado, apresentei uma versão básica de um ASP.NET MVC framework — o quadro de SimpleProgress — que permite que você rapidamente e eficazmente configure uma barra de progresso verdadeiramente sensível ao contexto (msdn.microsoft.com/magazine/hh580729). Sensível ao contexto é provavelmente apenas que uma barra de progresso deve ser. Ou seja, ele deve ser um elemento de interface do usuário que progressivamente descreve o status de uma operação em curso. A exibição progressiva pode ser tão simple como uma seqüência de mensagens ou pode ser mais convincente — por exemplo, um indicador.

Neste artigo, mostrarei como aprimorar a barra de progresso, adicionando recursos de cancelar. Em outras palavras, se o usuário interage com a interface e cancela a operação, o quadro vai dirigir o gerente da operação em curso para interromper qualquer trabalho a ser feito.

Cancelando tarefas em andamento

O cancelamento de uma tarefa de servidor em curso de dentro de um navegador do cliente não é uma operação trivial. Não se deixe engane por exemplos muito básicos que você encontrará que apenas anular a solicitação do cliente e fingir que tudo está desmarcado também no servidor.

Quando você aciona uma operação do servidor via AJAX, é aberto um soquete que conecta seu navegador para um ponto de extremidade remoto. Esta conexão permanece aberta, aguardando o pedido concluir e retornar uma resposta. O truque que abordei no mês passado para criação de uma barra de progresso sensível ao contexto usado um fluxo paralelo de solicitações para verificar um segundo método de controle responsável por retornar o status da operação — espécie de uma caixa de correio que cliente e servidor podem usar para se comunicar.

Agora suponha que há um botão anular que permite que o usuário cancelar a operação de servidor atual. Que tipo de código é que vai exigir? Na pior das hipóteses, você deseja anular a solicitação do AJAX. Se a operação do servidor foi iniciada usando jQuery AJAX API, você pode fazer o seguinte:

xhr.abort();

A variável xhr é a referência para o objeto XmlHttpRequest usado na chamada. Em jQuery, essa referência é retornada diretamente pela função $.ajax. Figura 1 mostra um trecho do quadro de progresso (renomeado ProgressBar) do código do mês passado que adiciona um novo método abort.

Figura 1 adicionando anular a funcionalidade para o quadro de progresso

var ProgressBar = function () {
  var that = {};
  // Store the XHR object being used.
that._xhr = null;// Get the user-defined callback that runs after
// aborting the call.
that._taskAbortedCallback = null;
...// Set progress callbacks.
that.callback = function (userCallback, completedCallback,
                          abortedCallback) {
  that._userDefinedProgressCallback = userCallback;
  that._taskCompletedCallback = completedCallback;
  that._taskAbortedCallback = abortedCallback;
  return this;
};
// Abort function.
that.abort = function () {
  if (_xhr !== null)
       xhr.abort();
};
...
// Invoke the URL and monitor its progress.
that.start = function (url, progressUrl) {
  that._taskId = that.createTaskId();
  that._progressUrl = progressUrl;
  // Place the AJAX call.
xhr = $.ajax({
      url: url,
      cache: false,
      headers: { 'X-ProgressBar-TaskId': that._taskId },
      complete: function () {
        if (_xhr.status != 0) return;
        if (that._taskAbortedCallback != null)
            that._taskAbortedCallback();
        that.end();
      },
      success: function (data) {
        if (that._taskCompletedCallback != null)
            that._taskCompletedCallback(data);
        that.end();
            }
  });
  // Start the progress callback (if any set).
if (that._userDefinedProgressCallback == null)
      return this;
  that._timerId = window.setTimeout(
    that._internalProgressCallback,
    that._interval);
};
  return that;
}

Como você pode ver, o novo método abort não faz muito além da chamada abort no objeto XmlHttpRequest interno. Abortar uma chamada em curso de AJAX ainda dispara o evento completo no Gestor de AJAX, no entanto, embora não quaisquer funções de sucesso ou erro. Para detectar se uma operação foi anulada pelo usuário, você anexa um manipulador para concluir e verificar a propriedade status do objeto XmlHttpRequest — será 0 se a operação foi anulada. O manipulador completo, em seguida, executa qualquer operação de limpeza é necessária — parar temporizadores, por exemplo. Figura 2 mostra uma interface agradável, de que os usuários podem parar de operações remotas à vontade.

Cancelable AJAX Operations
Figura 2 cancelável AJAX operações

Notificar o servidor

A interface do usuário agradável no Figura 2 não necessariamente garante que a operação do servidor foi interrompida como o usuário solicitado e como comentários do aplicativo parece provar. Chamada abort em XmlHttpRequest simplesmente fecha o soquete que conecta o navegador para o servidor. Dito de outra forma, por chamada abort em XmlHttpRequest, você simplesmente dizer que você não está mais interessado em receber qualquer resposta do método de servidor pode gerar. Nada realmente garante que o servidor recebeu e reconheceu o pedido de anulação; mais provavelmente, o servidor continuará a processar a solicitação independentemente de saber se um navegador está aguardando uma resposta. Embora este aspecto particular pode variar em diferentes plataformas e servidores, há outro aspecto a considerar que depende estritamente do seu aplicativo. Se o pedido desencadeou uma operação assíncrona ou uma operação de longa duração, você pará-lo? A verdade é que não há nenhuma maneira confiável e automática para interromper o processamento de um pedido; você tem que construir seu próprio framework e escrever seus métodos de servidor para ser passível de interrupção. Vamos ampliar a estrutura de progresso, então.

Alargar o âmbito de progresso

O componente Gerenciador de servidor é a parte do framework que controladores de trabalham com. Métodos chamam métodos na interface seguinte para postar mensagens para a barra de progresso do cliente e para receber notificações de interface do usuário para interromper o processamento:

public interface IProgressManager
{
    void SetCompleted(String taskId, String format, params Object[] args);
    void SetCompleted(String taskId, Int32 percentage);
    void SetCompleted(String taskId, String step);
    String GetStatus(String taskId);
    void RequestTermination(String taskId);
    Boolean ShouldTerminate(String taskId);
}

Em comparação com o código que apresentei no mês passado, há um par de métodos extras — RequestTermination, que clientes irão chamar a finalização do pedido, e ShouldTerminate, quais os métodos de ação irão chamar para ver se é suposto parar e reverter.

Cada progresso gerente trabalha em cima de um provedor de dados que contém o status das tarefas pendentes, cada uma identificada com um ID gerado pelo cliente. O provedor de dados padrão no código-fonte usa o ASP.NET cache para armazenar o status das tarefas. Ele cria uma entrada para cada tarefa e armazena a entrada dentro de um objeto do tipo TaskStatus, da seguinte forma:

public class TaskStatus
{
  public TaskStatus(String status) : this (status, false)
  {
  }
  public TaskStatus(String status, Boolean aborted)
  {
    Aborted = aborted;
    Status = status;
  }
  public String Status { get; set; }
  public Boolean Aborted { get; set; }
}

Quando chama o método de controlador SetCompleted, ele acaba salvando uma mensagem de status da tarefa no armazenamento subjacente. Antes de continuar com a próxima etapa, o método do controle verifica se tem que abortar.

Juntando as peças: O servidor

Vamos ver o que é preciso para criar um método de controle para uma operação de várias etapas, monitorável e interruptível. Você começa com uma classe de controlador de amostra que herda de ProgressBarController:

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

O método exemplo retorna uma Cadeia de caracteres para simplicidade; Ele pode ser uma visão parcial ou JSON ou qualquer outra coisa que você precisa que ela seja.

A classe base, mostrado na Figura 3, é uma maneira simples de dotar o controlador final com um monte de métodos comuns.

Nota, em particular, os Status e anular métodos que definem uma API pública e padrão para clientes jQuery chamar para consultar o status atual e a finalização do pedido. Usando uma classe base, você evitar ter que reescrever o código repetidamente.

Figura 3 A classe ProgressBar Super

public class ProgressBarController : Controller
{
  protected readonly ProgressManager ProgressManager;
  public ProgressBarController()
  {
    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 Status()
  {
    var taskId = GetTaskId();
    return ProgressManager.GetStatus(taskId);
  }
  public void Abort()
  {
    var taskId = GetTaskId();
    ProgressManager.RequestTermination(taskId);
  }
}

Figura 4 mostra o padrão para um método de controle que precisa ser monitorado do cliente.

Figura 4 controlador monitorizáveis Método usando o Framework de progresso

public String BookFlight(String from, String to)
{
  var taskId = GetTaskId();
  // Book first leg
  ProgressManager.SetCompleted(taskId,
    "Booking flight: {0}-{1} ...", from, to);
  Thread.Sleep(4000);
  if (ProgressManager.ShouldTerminate(taskId))
  {
    // Compensate here
    //
    return String.Format("One flight booked and then canceled");
  }
  // Book return flight
  ProgressManager.SetCompleted(taskId,     "Booking flight: {0}-{1} ...", to, from));
  Thread.Sleep(4000);
  if (ProgressManager.ShouldTerminate(taskId))
  {
    // Compensate here
    //
    return String.Format("Two flights booked and then canceled");
  }
  // Book return
  ProgressManager.SetCompleted(taskId,     "Paying for the flight ...", taskId));
  Thread.Sleep(5000);
  if (ProgressManager.ShouldTerminate(taskId))
  {
    // Compensate here
    //
    return String.Format("Payment canceled.
No flights booked.");
  }
  // Some return value
  return "Flight booked successfully";
}

A ID da tarefa é gerado no cliente e transmitido para o servidor por meio de um cabeçalho HTTP. O método GetTaskId protege os desenvolvedores de controlador de ter que saber esses detalhes. O método de controlador faz seu trabalho aos poucos e chama SetCompleted cada vez que realiza uma parte significativa do trabalho. Indica o trabalho feito usando uma Cadeia de caracteres, o que poderia ser uma percentagem, bem como uma mensagem de status. Periodicamente, o método do controle verifica se um pedido de rescisão foi recebido. Se esse for o caso, ele faz tudo o que é possível reverter ou compensar e, em seguida, retorna.

Juntando as peças: O cliente

No lado do cliente, você precisa de ligar o progresso Framework Java­API de Script e a biblioteca jQuery:

    <script src="@Url.Content("~/Scripts/progressbar-fx.js")"
            type="text/javascript"></script>

Cada monitorizáveis Método será chamado através de AJAX e, portanto, será acionado por um evento do cliente — por exemplo, um clique de botão, criado com marcação, como mostrado na Figura 5.

Figura 5 marcação para desencadear uma ação monitorável e interruptível

    <fieldset>
      <legend>Book a flight...</legend>
      <input id="buttonStart" type="button" value="Book a flight..." />
      <hr />
      <div id="progressbar_container">
        <span id="progressbar2"></span>
        <input id="buttonAbort" type="button"
          value="Abort flight booking"
          disabled="disabled" />
      </div>   
    </fieldset>
    Click handlers are attached unobtrusively when the page is loaded:
    <script type="text/javascript">
      var progressbar;
      $(document).ready(function () {
        $("#buttonStart").bind("click", buttonStartHandler);
        $("#buttonAbort").bind("click", buttonAbortHandler);
      });
    </script>

Figura 6 mostra o JavaScript para iniciar e anulando uma operação remota e atualizando a interface do usuário em conformidade.

Figura 6 o código JavaScript para a exibição de amostra

function buttonStartHandler() {
  updateStatusProgressBar ();
  progressbar = new ProgressBar();
  progressbar.setInterval(600)
             .callback(function (status) {
                             $("#progressbar").text(status); },
                       function (response) {
                             $("#progressbar").text(response);
                             updateStatusProgressBar(); },
                       function () {
                             $("#progressbar").text("");
                             updateStatusProgressBar(); })
             .start("/task/bookflight?from=Rome&to=NewYork",
                    "/task/status",
                    "/task/abort");
}
function buttonAbortHandler() {
    progressbar.abort();
}
function updateStatusProgressBar () {
    $("#buttonStart").toggleDisabled();
    $("#buttonAbort").toggleDisabled();
}

O objeto ProgressBar JavaScript consiste de três métodos principais.O método setInterval especifica o intervalo entre duas verificações sucessivas atualizações de Estado.O valor para transmitir está em milissegundos.O método de retorno de chamada define um grupo de funções de retorno de chamada para atualização do status e para atualizar a interface do usuário quando a operação for concluída com êxito, ou quando a operação for anulada pelo usuário.E o método start começa a operação.Ele leva três URLs: o ponto de extremidade do método a ser executado e os pontos de extremidade para os métodos ser chamado de volta para capturar atualizações de Estado e para anular a operação pendente.

Como você pode ver, as URLs são relativos e expressa no método/controlador/formulário.Naturalmente, você pode alterar os nomes do status de método e anular para o que quiser — contanto que tais métodos existam como pontos de extremidade públicos.Os métodos de status e anular são garantidos a existir se você herdar sua classe de controlador de ProgressBarController.Figura 7 mostra o aplicativo de exemplo em ação.Embora as UIs em Figura 2 e Figura 7 têm a mesma aparência, o código subjacente e comportamento são realmente diferentes.

The Framework in ActionFigura 7 O quadro em ação

A fazer progressos

AJAX fornece as ferramentas para sondar o servidor e perguntar o que está acontecendo.Você tem que construir seu próprio framework se desejar que ele para fornecer métodos monitorável e interruptíveis.Note-se que um exemplo real, o método de ação propriamente dito pode chamar outros serviços assíncronos.Esse código pode ser complicado.Felizmente, escrever código assíncrono deve obter mais simples o ASP programado.NET MVC 4.E na minha próxima coluna, mostrarei outra abordagem para implementação e monitoramento remotas tarefas com base em uma nova biblioteca de cliente que também poderia torná-lo para o ASP.NET MVC 4 pacote — a biblioteca SignalR.Por agora, obter o código-fonte do archive.msdn.microsoft.com/mag201201CuttingEdge e deixe-me saber a sua opinião!

Dino Esposito é o autor de “Programming Microsoft ASP.NET MVC3” (Microsoft Press, 2011) e coautor de “Microsoft .NET: Arquitetura de aplicativos para a empresa"(Microsoft Press, 2008). Residente na Itália, Esposito é palestrante assíduo em eventos do setor em todo o mundo. Você pode segui-lo no Twitter em twitter.com/despos.

Graças ao seguinte especialista técnico para rever esta coluna: Phil Haack