Desenvolvimento - C#

Construindo uma aplicação de Wizard assistente utilizando ASP.NET MVC

Este post tem como objetivo construir um assistente (wizard) para web, utilizando ASP.NET MVC, jQuery e o padrão de projetos Singleton. Existem várias formas de implementar este tipo de aplicação, esta é apenas uma delas, que poderia ainda contemplar DDD, TDD, entre outros padrões e técnicas.

por Flávio Henrique de Carvalho



Considero o uso de assistentes do tipo wizard muito indicado quando se tem algum tipo de funcionalidade em que o usuário vai construindo um objeto qualquer, interagindo com o sistema até atingir um objetivo final. A aplicação permite que o usuário interaja com algumas etapas (níveis), faça escolhas (configurações) em cada uma delas e na etapa final tem seu objeto montado ou uma funcionalidade executada. Este tipo de aplicação é muito utilizada em vários tipos de sistemas, além de ter uma grande variedade de formas para implementação. É comum vermos assistentes para instalação de algum programa, configuração de alguma funcionalidade de uma aplicação, importação de arquivos de entrada, sites de compras online, entre outros.

Este post tem como objetivo construir um assistente (wizard) para web, utilizando ASP.NET MVC, jQuery e o padrão de projetos Singleton. Existem várias formas de implementar este tipo de aplicação, esta é apenas uma delas, que poderia ainda contemplar DDD, TDD, entre outros padrões e técnicas. Porém para efeito de simplicidade focaremos em alguns pontos básicos, para que o objetivo principal seja atingido, esta é uma proposta de um algoritmo bastante simples e eficiente. Nada impede também de que as idéias aqui contidas sejam aperfeiçoadas com o uso de outras técnicas como as já mencionadas e outras. Ao final deste post será dado ao leitor um algoritmo básico para aplicações que utilizem este tipo de abordagem, tornando possível a construção de um wizard para uma funcionalidade qualquer.

Aplicações ASP.NET MVC tem as camadas Model, View e Controller, cujos conceitos não serão abordados neste artigo. Podendo ser encontrado uma vasta literatura para estudos em diversas outras fontes na internet, como o próprio site do ASP.NET MVC http://www.asp.net/mvc/tutorials/asp-net-mvc-overview-cs, ou em um outro post onde abordo sobre assunto, http://flaviohenriquedecarvalho.wordpress.com/2010/12/10/uma-visao-resumida-e-pratica-sobre-asp-net-mvc-2. Em nossa aplicação wizard temos um único Controller para a manipulação de todas as etapas do processo e uma View para cada etapa do processo. Com base nesta concepção se construirmos uma aplicação de três etapas (níveis), teremos um Controller, três Views e quantos Models forem necessários para representar as entidades que forem utilizadas.

O Controller deverá conter Action Methods (métodos de ação) com duas funções básicas, renderizar as Views de cada uma das etapas contidas no assistente, passando parâmetros para as mesmas através de ViewData’s. E receber as configurações definidas pelo usuário via Submit, ou requisição Ajax para inserção das entradas do usuário num ‘recipiente’ de dados. Aqui não estaremos persistindo os dados em nenhum banco de dados, ou arquivo, para não tornar mais complexo o artigo, mas na prática é necessário que se armazene as informações de alguma forma. A estrutura básica da classe Controller idealizada para esta aplicação ficou da seguinte forma:

public class MyWizardController : Controller

{

      public ActionResult LevelOne()

      {

          <Algum código aqui !!!>

               

return View();

      }

      [HttpPost]

      public ActionResult LevelOne(string Param1, … , string ParamN)

{

            try

            {

          <Algum código aqui !!!>

                if (Request.IsAjaxRequest()){

return Redirect(Url.Action("LevelTwo","MyWizard"));

                }

                return RedirectToAction("LevelTwo");

            }

            catch (Exception err)

            {

                ViewData["MessageError"] = err.Message;

                if (Request.IsAjaxRequest()){

                  return Redirect(Url.Action("LevelOne","MyWizard"));

                }

                return RedirectToAction("LevelOne");

}

}

      public ActionResult LevelTwo()

      {

          <Algum código aqui !!!>

               

return View();

      }

[HttpPost]

      public ActionResult LevelTwoPrevious()

      {

            return RedirectToAction("LevelOne");

      }

      [HttpPost]

      public ActionResult LevelTwo(string Param1, … , string ParamN)

{

            try

            {

          <Algum código aqui !!!>

                if (Request.IsAjaxRequest()){

return Redirect(Url.Action("LevelThree","MyWizard"));

                }

                return RedirectToAction("LevelThree");

            }

            catch (Exception err)

            {

                ViewData["MessageError"] = err.Message;

                if (Request.IsAjaxRequest()){

                  return Redirect(Url.Action("LevelTwo","MyWizard"));

                }

                return RedirectToAction("LevelTwo ");

}

}

<.. Outros métodos aqui !!!>

      public ActionResult LevelN()

      {

          <Algum código aqui !!!>

return View();

      }

[HttpPost]

      public ActionResult LevelNPrevious()

      {

            return RedirectToAction("LevelN-1");

      }

      [HttpPost]

      public ActionResult LevelN(string Param1, … , string ParamN)

{

            try

            {

        <Algum código aqui !!!>

                if (Request.IsAjaxRequest()){

return Redirect(Url.Action("LevelN+1","MyWizard"));

                }

                return RedirectToAction("LevelN+1");

            }

            catch (Exception err)

            {

                ViewData["MessageError"] = err.Message;

                if (Request.IsAjaxRequest()){

                  return Redirect(Url.Action("LevelN","MyWizard"));

                }

                return RedirectToAction("LevelN");

}

}

}

 Analisando a classe MyWizardController que implementa a classe Controller percebe-se que há um trio de métodos do tipo ActionResult para cada etapa de nosso assistente. Três métodos LevelTwo, três métodos LevelThree e assim sucessivamente. O primeiro método de cada etapa não possui parâmetros (isto não é uma obrigatoriedade) e chama através da instrução return a View que irá renderizar. Sendo assim, LevelOne() carrega a primeira View, LevelTwo() carrega a segunda View e assim por diante. Havendo necessidade de passar algum tipo de dado do Controller para a View, deve-se utilizar o container ViewData. E para ilustrar melhor esta situação, supomos que numa etapa do assistente fosse necessário exibir alguns dados de um banco de dados. Estes registros seriam lidos do banco e para exibição num dropDownlist seriam atribuídos a um ViewData através de um collection List. Internamente a View, dentro da marcação HTML chama-se o ViewData, de mesmo nome, no atributo Value do controle que se deseja exibi-lo. O link, http://msdn.microsoft.com/en-us/library/dd394711.aspx explica com mais detalhes como utilizar este recurso.

O segundo método do trio responsável pela etapa, vem decorado com o atributo HttpPost, que é usado para restringir um método de ação para tratar apenas requisições HTTP POST. Este fará o processo inverso do primeiro método, recebendo os dados inseridos na View, que são retornados para o método vinculado no Controller. Assim, as entradas do usuário são submetidas através de um botão do tipo Submit numa tag form da View, ou de um botão que dispara uma requisição Ajax. Este método pode ou não conter parâmetros, pois sua finalidade é receber dados enviados da View para o Controller. Porém este método também é responsável pela navegação de uma etapa para outra, ir para ‘frente’, para a próxima etapa dentro do assistente. Sendo assim, nem sempre é necessário passar algum dado para algum parâmetro, existem casos onde uma View exibe apenas informações, não havendo inserção de dados. Havendo algum parâmetro, este deverá receber o conteúdo um controle HTML da View (entrada de usuário), ou o resultado de algum processamento por ela gerado. Uma observação importante é que o parâmetro deve obrigatoriamente ter o nome do controle HTML para o caso de um botão Submit, ou ser tratado no atributo de Data da requisição Ajax através da atribuição. Seguindo adiante dentro deste artigo, será exibido a implementação de uma View com mais detalhes à este respeito.

Assim como o segundo método do trio, o terceiro tipo de método liga a View a um método do Controller. Este vem sempre sucedido da palavra Previous (LevelTwoPrevious, LevelThreePrevious, ...) e exatamente como seu nome indica serve para voltar a uma etapa anterior na navegação do assistente. Também vem decorado com o atributo HttpPost e pode ser utilizado para desfazer a ação executada pelo segundo método, implementando assim uma forma de Rollback. Observando que, LevelOne não possui um método LevelOnePrevious pelo simples fato de não haver uma etapa anterior a primeira etapa, assim como a última etapa não necessitará possuir um Action Method para avançar a última etapa.

Importante de se notar com relação a estes dois últimos métodos é a forma que eles conectam a lógica de navegação da aplicação. O entrelaçamento dos níveis se dá nestes métodos, chamando de dentro da instrução Try o próximo nível, em caso de sucesso. Ou exibindo o mesmo nível com uma mensagem de erro devidamente armazenada num ViewData em caso de problema. Destacando que para as chamadas das Views há uma tratativa especial para requisições Ajax ou não, seguindo as boas práticas de programação para requisições ‘Ajax Não Obstrutiva’.

O método Redirect para a chamada das Views procedentes das requisições Ajax foi reescrito para atender as requisições Ajax conforme está descrito no Post de Ricardo Rocha, http://aircord.wordpress.com/2010/11/10/redirecionamento-com-requisicoes-ajax/. Antes da utilização desta classe, havia um problema em que a View chamada na instrução return não era renderizada quando chamada por uma requisição Ajax. A classe exibida abaixo resolveu esta questão.

/* IMPORTANTE: Este método faz parte da tratativa realizada para requisição AJAX, que manipula métodos da classe JavaScriptRedirectResult. Este método está contido no mesmo Controller dos Níveis. */

protected override RedirectResult Redirect(string url)

{

return new SISNAT.Models.JavaScriptRedirectResult(url);

}

public class JavaScriptRedirectResult : RedirectResult

{

public JavaScriptRedirectResult(string url): base(url) { }

      public override void ExecuteResult(ControllerContext context)

      {

       if (context.RequestContext.HttpContext.Request.IsAjaxRequest())

       {

var destinationUrl = UrlHelper.GenerateContentUrl(Url, context.HttpContext);

            var result = new JavaScriptResult()

            {

                  Script = "window.location="" + destinationUrl + "";"

            };

                result.ExecuteResult(context);

       }

       else

       {

            base.ExecuteResult(context);

       }

}

}

Feito isto, têm-se então a lógica de navegação entre as etapas do assistente concluída. Sempre lembrando que para cada etapa do nosso assistente será renderizada uma View. Existem outras abordagens onde poderíamos criar todas as etapas numa mesma View fazendo a navegação via jQuery (Javascript), mas neste caso optou-se por utilizar a estrutura de exibição de várias Views do MVC. Considero mais apropriado pela própria natureza conceitual deste framework que trata visualizações de diferentes marcações HTML para que sejam exibidas em diferentes Views.

Até este momento nosso código preocupou-se unicamente em mostrar os Action Methods para a navegação entre Views (etapas do assistente), não se importando em exibir algum tipo de funcionalidade. É claro que deve existir um objetivo funcional para a construção de um wizard. Na prática, os métodos do Controller devem vir ‘recheados’ de chamadas a funções do domínio da aplicação, mas a intenção neste caso é apenas mostrar um modelo genérico para aplicações wizard. Por isto a despreocupação em criar algum tipo de código no Controller, que trataria e manipularia outras funções e entradas do usuário.

Na primeira versão deste assistente ainda não existia a percepção da arquitetura de classes que estava por vir. Sendo assim, foram sendo criadas variáveis que armazenavam estaticamente as entradas de usuário no decorrer das etapas. Esta prática tinha como objetivo armazenar as entradas para que na etapa final houvessem algumas variáveis preenchidas com os dados do decorrer do processo. Mas pensando em orientação objetos, a opção mais lógica é encapsular estas variáveis numa segunda classe chamada ConfigWizard, onde cada antiga variável armazenadora de entradas passa à ser um campo/propriedade desta classe. O que é uma conseqüência lógica, tendo em vista que tais variáveis compunham uma entidade única armazenadora das configurações escolhidas.

A evolução de ConfigWizard veio com a percepção de que esta classe é única em todo o decorrer do processo. Ou seja, a classe instanciada inicialmente na etapa um do assistente, deve ir sendo manipulada e consultada até a etapa final. De posse do conhecimento do conceito do padrão de projeto Singleton, http://pt.wikipedia.org/wiki/Singleton, que garante a existência de apenas uma instância da classe. Concluiu-se então que a aplicação do Pattern Singleton para esta classe seria uma boa prática. Seguindo esta lógica, a classe ConfigWizard ficou da seguinte forma:

public class ConfigWizard

{

private string _Id;

<declaração de outros campos necessários>

      // Métodos específicos do Padrão.

      #region Singleton Pattern Implementation

      private static ConfigWizard _Instance = null;

      private static object _objLock = new object();

           

//O private garante que este construtor só será chamado de dentro da classe, evitando assim criar uma outra instância!

      private ConfigSpeciesImporter()

      {

            _Id = Guid.NewGuid().ToString();

      }

//Para utilizar o Singleton, devemos utilizar este método, ele garantirá a existencia de apenas uma instância para a classe.

      public static ConfigWizard getInstance()

      {

            lock (_objLock){

                  if (_Instance == null){

                        _Instance = new ConfigWizard ();

                  }

                  return _Instance;

            }

      }

      #endregion

       

      // PROPERTIES

      public string Id

      {

            get { return _Id; }

            private set { _Id = value; }

      }         

<GET e SET de outras propriedades>

// METHODS

      public void Dispose()

      {

            _objLock = new object();

            _Instance = null;

      }

<Outros métodos desta classe para manipulação de seus campos>

}

A classe ConfigWizard é uma classe Singleton e mais uma vez não serão abordados detalhes conceituais a este respeito, porém é importante a definição do método Dispose() pela importância de sua criação dentro do contexto da aplicação exemplificada. Inicialmente a classe ConfigWizard não possuía este método, mas com a utilização da classe foi observado uma falha relativamente grave. Com a conclusão do assistente, ou seja, finalização da etapa final, não se finalizava a instância de ConfigWizard. Mesmo se dirigindo para outras partes do sistema, ao voltar para o assistente, iniciando na primeira etapa (LevelOne), a classe era instanciada com as propriedades carregadas com antigas entradas. A solução foi a adaptação de um método que inicializava _objLock e _Instance ao iniciar o assistente. Assim, a classe é instanciada globalmente dentro da classe Controller para que todos os métodos possam acessá-la:

public class MyWizardController : Controller

{

// Classe de configurações selecionadas pelo usuário no decorrer da importação.

ConfigWizard ConfigImporter = ConfigWizard .getInstance();

...

         E no primeiro Action Method do Assistente adiciona-se a chamada do Dispose() da classe ConfigWizard:

[HttpPost]

public ActionResult LevelOne()

{

            /* IMPORTANTE: O método Dispose de ConfigSpecieImporter foi criado com objetivo de "zerar" a classe para que uma importação não herde atributos de outras importações anteriores. */

ConfigImporter.Dispose();

...

         Com isto finalizamos a lógica sobre a classe de armazenamento das entradas (ConfigWizard) de usuário em nosso Controller. Cujo objetivo básico é funcionar como um recipiente de informações da etapa inicial a etapa final, perdurando apenas dentro deste escopo. Como já definido em nossas regras de negócio existe uma View para cada etapa do assistente, renderizadas por Action Methods do Controller, LevelOne, LevelTwo, etc. As Views possuem uma marcação HTML básica definida abaixo, independente de sua característica principal e seus controles de entradas.

<html xmlns="http://www.w3.org/1999/xhtml" >

    <head runat="server">

        <title>Exemplo de aplicação Wizard para Web</title>

    </head>

    <body>         

<div>

            <h3>

                  <strong>Etapa 5 de 8: </strong>

</h3>               

      </div>

               

<% using (Html.BeginForm("LevelFive", "MyWizard", FormMethod.Post, new { enctype = "multipart/form-data" }))

      { %>

            <p>

                  Selecione um produto: <%= Html.DropDownList("Prod")%>

</p>

<!-- Marcação HTML responsável por receber as entradas de usuário.  -->                

      <% }%>

               

<div class="btnWizard">

<% using (Html.BeginForm("LevelFivePrevious", "TreeImports", FormMethod.Post, new { enctype = "multipart/form-data" }))

            { %>  

            <!—- Botão que volta a etapa anterior do assistente -->

<input id="btnPrevious" type="submit" value="<< Anterior" />

<%} %>

           

            <!—- Botão que chama a próxima etapa do assistente  -->  

            <input id="btnNext" type="submit" value="Próximo >>" />

      </div>

    </body>

</html>

         Nas primeiras linhas desta View exibi-se as tags <html>, <head>, <title>, <body>, que definem a estrutura básica de uma página HTML. Em seguida utilizou-se um header H3 para orientar o usuário à respeito do número da etapa em que se está posicionado dentro das etapas e o número da etapa final.

          O Helper Html.BeginForm é utilizado para renderizar um elemento Form HTML e todos os controles de entradas deverão estar contidos nele. Desta forma, teremos tags de elementos Labels, TextBox, DropDownList, etc... inseridos internamente a Html.BeginForm. Observando que o primeiro parâmetro de Html.BeginForm, LevelFive, aponta para o Action Method que será submetido por ele no Controller. O parâmetro MyWizard aponta para o Controller que contêm o Action Method LevelFive. O terceiro parâmetro se refere à forma de envio dos dados nos controles HTML para o servidor, neste caso, utilizamos do método Post. Que não por coincidência deverá vir decorando o método de ação no Controller, assim:

[HttpPost]

public ActionResult LevelFive (...)

{

...

Na marcação HTML que vem à seguir foi adicionado uma Div para inserção dos botões Previous /Next com o atributo class igual à btnWizard, afim de poder dar uma formatação CSS específica aos botões de navegação. O botão btnPrevious foi inserido num Html.BeginForm que aponta para outro método de ação chamado LevelFivePrevious, similar ao método LevelFive() explicado anteriormente. Para que o botão btnNext não aparecesse anterior a btnPrevious ele foi tirado do primeiro HTML.BeginForm e passou a não responder pelo Submit do Form. Embora torne um pouco mais trabalhosa para a implementação tirar btnNext do Helper HTML.BeginForm LevelFive, graficamente passamos a ter um interface com os botões de navegação numa disposição mais lógica, onde o botão ‘Anterior’ vem seguido do botão ‘Próximo’ dentro da idéia de um assistente.

Ao extrair btnNext de Html.BeginForm, ele passa a não responder mais através do Submit do Form, como já foi dito. A rotina abaixo foi construída utilizando um método de requisição Ajax, utilizando seletores jQuery dentro da tag <body> para este botão que o evento clique do botão dispare o Action Method do Controller.

    <script type="text/javascript">

        $(document).ready(function() {

            $("#btnNext").click(function() {

                $.ajax({

                    type: "post",

  url: "<%= Url.Action("LevelFive","MyWizard") %>",

  data: {

"Produto": $("# Prod option:selected").text()

},

                  error: function(response) {

alert("Ocorreu um erro na chamada da próxima etapa, por favor, tente novamente (" + response.toString() + ")!");

                    }

                });

            });

        });               

    </script>

Neste exemplo é invocado o Action Method de nome LevelFive localizado no Controller MyWizard conforme nos mostra o atributo url da Requisição Ajax. O atributo data enviará um parâmetro chamado Produto que recebe do seletor jQuery o conteúdo do DropDownList Prod. Assim, devemos ter um parâmetro chamado Produto em LevelFive, algo assim:

[HttpPost]

public ActionResult LevelFive(string Produto)

{

       

Desta forma finalizamos a aplicação com sua estrutura básica, conscientes de que para uma aplicação ‘real’ ainda faltariam classes representando as entidades da aplicação em Model, ou num projeto de domínio. Repositórios para a persistência dos dados no banco de dados ou em algum tipo de arquivo XML, XLS, MDB, etc. Ou mesmo algumas funcionalidades internas ao Controller MyWizard capazes de manipular os dados. Porém isto tornaria este artigo muito maior, fugindo a proposta inicial do projeto.

Flávio Henrique de Carvalho

Flávio Henrique de Carvalho - Bacharel em Ciência da Computação pela universidade Paulista de Ribeirão Preto. Trabalha com desenvolvido na plataforma Microsoft à 10 anos, para o desenvolvimento de aplicações de pesquisa na área de engenharia florestal. Focado em aplicações estatísticas e processamento de grandes massas de dados.

Visite seu blog,
http://flaviohenriquedecarvalho.wordpress.com.