ASP.NET

Apresentando a navegação para a estrutura dos Web Forms do ASP.NET

Graham Mendick

Baixar o código de exemplo

A Navegação para a estrutura de Web Forms do ASP.NET, um projeto de software livre hospedado em navigation.codeplex.com, permite escrever código de Web Forms com cobertura de teste de unidade e aderência ao princípio de DRY ("Não ser repetitivo"), que tornaria um aplicativo ASP.NET MVC verde de inveja.

Embora tenha havido algum abandono dos Web Forms em favor do MVC, com alguns desenvolvedores ficando cansados de grandes code-behinds inatingíveis por testes de unidade, essa nova estrutura, em conjunto com a vinculação de dados, é um argumento convincente para revisitar os Web Forms.

A vinculação de dados com controles ObjectDataSource tem sido usada desde o Visual Studio 2005, permitindo code-behinds mais limpos e que o código de recuperação de dados passe por teste de unidade, mas ocorreram problemas que inibiram a sua adoção, por exemplo, a geração de uma exceção era a única maneira de relatar uma falha de validação de negócios de volta para a interface do usuário.

A grande maioria dos esforços de desenvolvimento dos Web Forms para a futura versão do Visual Studio foi investida em vinculação de dados, introduzindo conceitos de vinculação de modelos do MVC para resolver esses problemas, por exemplo, a introdução do estado de modelo resolve o problema de comunicação de falha na validação dos negócios. No entanto, ainda existem dois espinhos no lado da vinculação de dados relacionados à navegação e à transmissão de dados, mas eles pode ser facilmente extraídos usando a estrutura de navegação para os Web Forms do ASP.NET (que chamarei de "estrutura de navegação" daqui em diante por questões de brevidade).

O primeiro espinho é que não há nenhuma abstração da lógica de navegação, ao contrário do MVC, onde ela é encapsulada nos tipos de retorno do método do controlador. Isso resulta em chamadas de redirecionamento dentro dos métodos de vinculação de dados, impedindo que eles passem por um teste de unidade. O segundo espinho é que o tipo de um parâmetro de um ObjectDataSource determina de onde vem seu valor, por exemplo, um QueryStringParameter sempre obtém seus dados da cadeia de caracteres de consulta. Isso impede que as mesmas fontes de dados sejam usadas em diferentes contextos de navegação, como postback e não postback, sem lógica substancial no temido code-behind.

A estrutura de navegação remove esses espinhos utilizando uma abordagem holística da navegação e da transmissão de dados. Independentemente do tipo de navegação que está sendo executada, seja hiperlink, postback, histórico do AJAX ou teste de unidade, os dados que estão sendo transmitidos são sempre mantidos da mesma maneira. Em artigos futuros, mostrarei como isso leva a code-behinds vazios com recuperação de dados com teste de unidade completo e lógica de navegação e também à SEO (Otimização de Mecanismo de Pesquisa) amigável, aplicativos de uma única página progressivamente melhorados sem nenhuma duplicação de código para cenários habilitados e desabilitados para JavaScript. Este artigo apresenta a estrutura de navegação e demonstra alguns dos seus conceitos básicos, mas importantes, com a criação de um aplicativo Web de exemplo.

Aplicativo de exemplo

O aplicativo Web de exemplo é uma pesquisa online. Essa pesquisa tem apenas duas perguntas e exibe uma mensagem “thank you” na conclusão. Cada pergunta é representada por uma página ASPX separada chamada Question1.aspx e Question2.aspx respectivamente, e a mensagem “thank you” tem sua própria página chamada Thanks.aspx.

A primeira pergunta é “Which ASP.NET technology are you currently using?” para a qual as respostas possíveis são “Web Forms” ou “MVC”. Portanto, para Question1.aspx, adicionarei a pergunta e as respostas em botões de opção inseridas no código:

    <h1>Question 1</h1>
    <h2>Which ASP.NET technology are you currently using?</h2>
    <asp:RadioButtonList ID="Answer" runat="server">
      <asp:ListItem Text="Web Forms" Selected="True" />
      <asp:ListItem Text="MVC" />
    </asp:RadioButtonList>

Para a segunda pergunta, “Are you using the Navigation for ASP.NET Web Forms framework?”, as respostas são “Yes” ou “No” e estão marcadas de maneira semelhante.

Introdução

A maneira mais direta de configurar o projeto Web de pesquisa para usar a estrutura de navegação é instalá-lo usando o NuGet Package Manager. A execução do comando “Install-Package Navigation” no Package Manager Console adicionará a referência e a configuração necessárias. Se estiver usando o Visual Studio 2010, as instruções de configuração manual poderão ser encontradas em navigation.codeplex.com/documentation.

Configuração da navegação

A estrutura de navegação pode ser considerada como uma máquina de estado, onde cada estado diferente representa uma página, e a movimentação de um estado para o outro, ou a navegação entre as páginas, é chamada de transição. Esse conjunto predefinido de estados e transições é configurado no arquivo StateInfo.config criado pela instalação do NuGet. Sem essa configuração básica, a execução do aplicativo de pesquisa gerará uma exceção.

Como os estados são essencialmente apenas páginas, o aplicativo de pesquisa precisará de três estados, um para cada uma das três páginas:

    <state key="Question1" page="~/Question1.aspx">
    </state>
    <state key="Question2" page="~/Question2.aspx">
    </state>
    <state key="Thanks" page="~/Thanks.aspx">
    </state>

De agora em diante, farei referência aos diferentes estados por seus nomes principais, Question1, Question2 e Thanks, e não pelas páginas que representam.

Como as transições descrevem as possíveis navegações entre os estados, o aplicativo de pesquisa precisa de duas transições. Uma é para a navegação de Question1 para Question2, e a outra é para a navegação de Question2 para Thanks. Uma transição aparece como um filho do estado do qual está saindo e aponta, por meio de seu atributo "to", para o estado no qual está entrando:

    <state key="Question1" page="~/Question1.aspx">
      <transition key="Next" to="Question2"/>
    </state>
    <state key="Question2" page="~/Question2.aspx">
      <transition key="Next" to="Thanks"/>
    </state>
    <state key="Thanks" page="~/Thanks.aspx">
    </state>

As caixas de diálogo são o elemento final da configuração e representam um agrupamento lógico de estados. O aplicativo de pesquisa precisa de apenas uma caixa de diálogo porque Question1, Question2 e Thanks são efetivamente um único caminho de navegação. O atributo “initial” da caixa de diálogo deve apontar para o estado inicial, isto é, Question1:

    <dialog key="Survey" initial="Question1" path="~/Question1.aspx">
      <state key="Question1" page="~/Question1.aspx">
        <transition key="Next" to="Question2"/>
      </state>
      <state key="Question2" page="~/Question2.aspx">
        <transition key="Next" to="Thanks"/>
      </state>
      <state key="Thanks" page="~/Thanks.aspx">
      </state>
    </dialog>

Você observará que cada caixa de diálogo, estado e transição tem um atributo key. Optei por dar nome às state keys de acordo com os nomes das páginas, mas isso não é necessário. No entanto, observe que todas as chaves devem ser exclusivas para seu pai, por exemplo, não é possível ter estados irmãos com a mesma chave.

Com o Question1.aspx como a página inicial, o aplicativo de pesquisa agora é iniciado com êxito no estado Question1. No entanto, a pesquisa permanecerá paralisada nesse estado porque não há como progredir para Question2.

É útil separar os diferentes tipos de navegação de Web Forms em dois grupos. O grupo não postback é onde o controle é passado de uma página ASPX para outra e toma a forma de hiperlinks, redirecionamentos ou transferências. O grupo postback é onde o controle permanece na mesma página e toma a forma de postbacks, solicitações de página parcial ou de histórico do AJAX. Esse segundo tipo será examinado em um artigo futuro, que discutirá o padrão de interface de página única. Neste artigo, focalizarei o primeiro tipo de navegação.

Para mover entre as páginas, deve ser criada uma URL. Antes do Visual Studio 2008, a única opção era construir URLs manualmente a partir dos nomes das páginas ASPX inseridas no código, causando um acoplamento rígido entre as páginas, o que tornava os aplicativos frágeis e difíceis de manter. A introdução do roteamento minimizou esse problema, com nomes de rotas configuráveis usadas no lugar de nomes de páginas. No entanto, o fato de que o roteamento gera uma exceção quando usado fora de um ambiente Web, combinado com a resistência à simulação do roteamento, o torna um inimigo do teste de unidade.

A estrutura de navegação retém o acoplamento menos rígido fornecido pelo roteamento e é amiga do teste de unidade. De maneira semelhante ao uso de nomes de rotas, em vez de inserir os nomes das páginas ASPX no código, a caixa de diálogo e as chaves de transição configuradas na seção anterior é que são referenciadas no código; o estado para o qual navegar é determinado pelos respectivos atributos “initial” e “to”.

Retornando à pesquisa, a transition key Next pode ser usada para mover de Question1 para Question2. Adicionarei um botão Next a Question1.aspx e o código a seguir a seu manipulador de cliques associado:

protected void Next_Click(object sender, EventArgs e)
{
  StateController.Navigate("Next");
}

A chave passada no método Navigate é correspondida com as transições filho configuradas do estado Question1 e, em seguida, o estado identificado pelo atributo "to" é exibido, isto é, Question2. Adicionarei o mesmo botão e o manipulador a Question2.aspx. Se você executar a pesquisa, descobrirá que pode navegar pelos três estados clicando nos botões Next.

Você deve ter notado que a segunda pergunta é específica aos Web Forms e, portanto, é irrelevante quando “MVC” é selecionado como a primeira resposta. O código precisa ser alterado para manipular esse cenário, navegando diretamente de Question1 para Thanks e ignorando completamente o Question2.

A configuração atual não permite navegação de Question1 para Thanks porque a única transição listada é para o Question2. Portanto, vou alterar a configuração adicionando a segunda transição abaixo do estado Question1:

    <state key="Question1" page="~/Question1.aspx">
      <transition key="Next" to="Question2"/>
      <transition key="Next_MVC" to="Thanks"/>
    </state>

Com essa nova transição estabelecida é fácil ajustar o manipulador de cliques do botão Next para passar uma transition key diferente de acordo com a resposta escolhida:

if (Answer.SelectedValue != "MVC")
{
  StateController.Navigate("Next");
}
else
{
  StateController.Navigate("Next_MVC");
}

Uma pesquisa não será muito boa se não permitir que o usuário altere as respostas. Atualmente, não existe nenhuma maneira de retornar a uma pergunta anterior (além do botão Voltar do navegador). Para navegar de volta, você precisa adicionar duas transições abaixo de Thanks, uma apontando para Question1 e Question2, e outra abaixo de Question2 apontando para Question1. Embora isso possa funcionar, é desnecessário porque a navegação regressiva vem de graça na Estrutura de navegação.

A navegação estrutural é um conjunto de links que fornecem acesso a cada página anterior visitada pelo usuário para atingir a atual. Os Web Forms têm a navegação estrutural incorporada em sua funcionalidade de mapa estrutural. No entanto, como os mapas de site são representados por uma estrutura de navegação fixa, para uma determinada página essa navegação estrutural é sempre a mesma independentemente da rota seguida. Não é possível manipular situações como a encontrada na pesquisa, onde a rota para Thanks algumas vezes exclui o Question2. A estrutura de navegação, mantendo o controle dos estados visitados à medida que a navegação ocorre, cria uma trilha de navegação estrutural da rota real seguida.

Para demonstrar, adicionarei um hiperlink ao Question2.aspx e, no code-behind, definirei sua propriedade NavigateUrl de forma programática usando a navegação regressiva. Um parâmetro de distância deve ser passado indicando quantos estados regredir para voltar, um valor de 1 significa o predecessor imediato:

protected void Page_Load(object sender, EventArgs e)
{
  Question1.NavigateUrl = StateController.GetNavigationBackLink(1);
}

Se você executar o aplicativo e responder “Web Forms” para a pergunta um, verá que o hiperlink do Question2.aspx o levará de volta para a primeira pergunta.

Farei o mesmo para o Thanks.aspx, embora isso seja um pouco mais complicado porque dois hiperlinks são necessários (um para cada pergunta), e o usuário pode não ter visto as duas perguntas, isto é, se tiver respondido “MVC” para a primeira. O número de estados precedentes pode ser verificado antes de decidir como configurar os hiperlinks (consulte a Figura 1).

Figura 1 Navegação regressiva dinâmica

protected void Page_Load(object sender, EventArgs e)
{
  if (StateController.CanNavigateBack(2))
  {
    Question1.NavigateUrl = StateController.GetNavigationBackLink(2);
    Question2.NavigateUrl = StateController.GetNavigationBackLink(1);
  }
  else
  {
    Question1.NavigateUrl = StateController.GetNavigationBackLink(1);
    Question2.Visible = false;
  }
}

A pesquisa agora está funcional, permitindo que você preencha perguntas e corrija respostas anteriores. Mas uma pesquisa onde as respostas não são usadas não faz muito sentido. Mostrarei como elas podem ser passadas de Question1 e de Question2 para Thanks, onde serão exibidas no formulário de resumo.

Passagem de dados

Há tantas maneiras diferentes de transmitir dados em Web Forms quanto há maneiras de navegar. Com a navegação não postback, onde o controle é passado de uma página para outra (por meio de hiperlink, redirecionamento ou transferência), a cadeia de caracteres de consulta ou os dados da rota podem ser usados. Antes da navegação postback, onde o controle permanece na mesma página (por meio de postback, solicitação de página parcial ou histórico do AJAX), os valores de controle, o estado da exibição ou os argumentos de event são os candidatos prováveis.

Antes do Visual Studio 2005, os code-behinds eram sobrecarregados com a manipulação desses dados passados e, portanto, aumentaram com a lógica de extração de valores e de conversão de tipos. Sua carga foi consideravelmente reduzida com a introdução de controles de fonte de dados e parâmetros de seleção (“provedores de valores” na próxima versão do Visual Studio). No entanto, esses parâmetros de seleção estão amarrados a uma fonte de dados específica e não podem alternar fontes dinamicamente dependendo do contexto de navegação. Por exemplo, eles não podem recuperar seus valores de maneira alternativa de um controle ou da cadeia de caracteres de consulta, dependendo de serem um postback ou não. Trabalhar com essas limitações faz com que o código vaze de volta para o code-behind, revertendo para a estaca zero de code-behinds sobrecarregados, que não podem ser testados.

A estrutura de navegação evita esses problemas, fornecendo uma única fonte de dados independentemente da navegação envolvida, chamada de dados de estado. Na primeira vez que a página é carregada, os dados de estado são populados com todos os dados passados durante a navegação, de maneira semelhante à cadeia de caracteres de consulta ou aos dados de rota. Uma diferença marcante, no entanto, é que os dados de estado não são somente leitura, portanto, quando ocorrem navegações postback subsequentes, eles podem ser atualizados para refletir a encarnação atual da página. Isso provará ser benéfico quando eu revisitar a navegação no final desta seção.

Alterarei a pesquisa para que a resposta à primeira pergunta seja passada para o estado Thanks onde será reexibida para o usuário. Os dados são passados durante a navegação por meio de uma coleção de pares de chave-valor, chamados de NavigationData. Alterarei o manipulador de cliques Next do Question1.aspx para que a resposta à primeira pergunta seja passada para o próximo estado:

NavigationData data = new NavigationData();
data["technology"] = Answer.SelectedValue;
if (Answer.SelectedValue != "MVC")
{
  StateController.Navigate("Next", data);
}
else
{
  StateController.Navigate("Next_MVC", data);
}

Esse NavigationData passado durante a navegação é usado para inicializar os dados de estado que são disponibilizados para o próximo estado por meio da propriedade Data do objeto StateContext. Adicionarei um rótulo ao Thanks.aspx e definirei sua propriedade Text para exibir a resposta passada em:

Summary.Text = (string) StateContext.Data["technology"];

Se você executar a pesquisa, observará que essas informações de resumo são exibidas apenas quando a resposta à primeira pergunta é “MVC”; uma resposta de “Web Forms” nunca é mostrada. Isso é porque o NavigationData só está disponível para o próximo estado, mas não para qualquer estado atingido como resultado da navegação subsequente. Portanto, uma resposta de “Web Forms” está presente nos dados do estado Question2, mas não está disponível quando Thanks é atingido. Uma maneira de resolver isso é alterar o Question2.aspx para que retransmita a resposta à primeira pergunta, isto é, obtenha a resposta de seus dados de estado e a passe para Thanks durante a navegação:

NavigationData data = new NavigationData();
data["technology"] = StateContext.Data["technology"];
StateController.Navigate("Next", data);

Essa abordagem não é a ideal, porque acopla o Question1 e o Question2, forçando o último estado a ficar ciente dos dados que estão sendo passados pelo anterior. Por exemplo, uma nova pergunta não pode ser inserida entre a primeira e a segunda sem uma alteração correspondente no Question2.aspx. Uma implementação de longo prazo envolve a criação de um novo NavigationData contendo todos os dados de estado Question2. Isso é realizado passando true para o construtor de NavigationData:

NavigationData data = new NavigationData(true);
StateController.Navigate("Next", data);

Outra diferença importante entre os dados de estado e a cadeia de caracteres de pesquisa ou dados de rota é que com os dados de estado você não está restrito às cadeias de caracteres que estão sendo passadas. Em vez de passar a resposta como uma cadeia de caracteres, como foi feito para o Question1, para o Question2 passarei um bool para Thanks com um valor de true, que corresponde a “Yes”:

NavigationData data = new NavigationData(true);
data["navigation"] = Answer.SelectedValue == "Yes" ? true : false;
StateController.Navigate("Next", data);

Você pode ver que seu tipo de dados é preservado quando é recuperado dos dados do estado Thanks:

Summary.Text = (string) StateContext.Data["technology"];
if (StateContext.Data["navigation"] != null)
{
  Summary.Text += ", " + (bool) StateContext.Data["navigation"];
}

A pesquisa é concluída, exceto por um problema: As respostas às perguntas não são mantidas ao usar os hiperlinks da navegação regressiva. Por exemplo, ao retornar para o Question1 a partir de Thanks, o contexto é perdido e, portanto, o botão de opção padrão “Web Forms” sempre é selecionado independentemente da resposta fornecida.

Na seção anterior, você viu o benefício da navegação regressiva em relação à navegação estrutural de mapa de site estático. Outra limitação da navegação estrutural gerada pelo mapa do site é que ela não carrega nenhum dado. Isso significa que, ao segui-la, as informações contextuais podem ser perdidas. Por exemplo, eles não podem passar a resposta “MVC” selecionada anteriormente quando retornam ao Question1 a partir de Thanks. A estrutura de navegação, ao manter o controle dos dados de estado associados aos estados visitados conforme ocorrem as navegações, cria uma trilha de navegação estrutural contextual. Durante uma navegação regressiva, esses dados de estado são restaurados, permitindo que a página seja recriada exatamente como antes.

Armado com a navegação regressiva contextual, posso alterar a pesquisa de forma que as respostas sejam mantidas ao revisitar os estados. O primeiro estágio é definir as respostas como dados de estado nos manipuladores de cliques de Next, antes da navegação para outro lugar:

StateContext.Data["answer"] = Answer.SelectedValue;

Agora, quando o Question1 ou o Question2 forem revisitados, os dados de estado conterão a resposta selecionada anteriormente. Assim, fica fácil recuperar essa resposta no método Page_Load e pré-selecionar o botão de opção relevante:

protected void Page_Load(object sender, EventArgs e)
{
  if (!Page.IsPostBack)
  {
    if (StateContext.Data["answer"] != null)
    {
      Answer.SelectedValue = 
        (string)StateContext.Data["answer"];
    }
  }
}

A pesquisa, agora completa, não está suscetível aos erros comumente encontrados em aplicativos Web quando os usuários pressionam o botão voltar do navegador (ou têm várias janelas do navegador abertas). Normalmente, esses problemas surgem quando dados específicos à página são persistidos em uma sessão no lado do servidor. Embora exista apenas um objeto de sessão, podem haver várias versões “atuais” de uma única página. Por exemplo, o uso do botão voltar para recuperar uma versão “obsoleta” de uma página do cache do navegador pode fazer com que o cliente e o servidor fiquem dessincronizados. A estrutura de navegação não enfrenta esses problemas porque não tem nenhum cache no lado do servidor. Em vez disso, o estado, os dados de estado e a trilha da navegação estrutural são todos mantidos na URL. No entanto, isso significa que um usuário pode alterar esses valores editando a URL.

Causando inveja no MVC

Anteriormente, eu disse que a estrutura de navegação permite criar código de Web Forms que causam inveja ao MVC. Depois dessa afirmação ousada, você deve estar se sentindo um pouco ludibriado pelo aplicativo de pesquisa de exemplo, porque ele provavelmente tem o MVC tampando o nariz para não sentir o cheiro de code-behind nele. Mas não se desespere, essa foi uma mera introdução aos conceitos principais. Os artigos futuros focalizarão a integridade arquitetural, com atenção especial ao teste de unidade e aos princípios de DRY.

No segundo artigo criarei um exemplo vinculado a dados com code-behinds vazios e cobertura completa de código de teste de unidade. Essa cobertura incluirá até o código de navegação, que é notoriamente difícil de testar em um aplicativo MVC.

No terceiro artigo criarei um aplicativo de uma única página amigável para a SEO. Usarei melhoria progressiva, utilizando o AJAX ASP.NET quando o JavaScript estiver habilitado e degradando normalmente quando estiver desabilitado, com os mesmos métodos de vinculação de dados usados nos dois cenários. Mais uma vez, isso é complicado de realizar em um aplicativo MVC.

Se isso despertar seu apetite e você não puder esperar para experimentar algumas das funcionalidades mais avançadas, baixe a documentação abrangente do recurso e o código de exemplo em navigation.codeplex.com.

Graham Mendick é o maior fã dos Web Forms e deseja mostrar que ele pode ser tão robusto arquiteturalmente quanto o ASP.NET MVC. Ele escreveu a estrutura de Web Forms do ASP.NET, que acredita que, quando usada com a vinculação de dados, pode dar vida nova aos Web Forms.

Agradecemos ao seguinte especialista técnico pela revisão deste artigo: Damian Edwards