Share via


ASP.NET

Teste de unidade na estrutura de navegação para ASP.NET Web Forms

Graham Mendick

Baixar o código de exemplo

Baixar a biblioteca

A estrutura Navegação para ASP.NET Web Forms, um projeto de software livre hospedado em navigation.codeplex.com, abre novas possibilidades para escrever aplicativos Web Forms ao assumir uma nova abordagem de navegação e passagem de dados. No código Web Forms tradicional, a maneira que os dados são passados é dependente da navegação executada. Por exemplo, pode acontecer em um cadeia de caracteres de consulta ou dados da rota durante um redirecionamento, mas em valores de controle ou estado de exibição durante um postback. No entanto, na estrutura Navegação para ASP.NET Web Forms (“Estrutura de Navegação” daqui em diante para fins de brevidade) uma única fonte de dados é usada em todos os cenários.

Em meu primeiro artigo (msdn.microsoft.com/magazine/hh975349), introduzi a estrutura de Navegação e criei um aplicativo de pesquisa online de exemplo para demonstrar alguns de seus principais conceitos e as vantagens que ela oferece. Em particular, mostrei como ela habilitava a geração de um conjunto de hiperlinks estruturais, contextuais e dinâmicos permitindo que um usuário retornasse a problemas anteriormente vistos com as respostas do usuário restauradas, superando as limitações da funcionalidade de mapa do site do ASP.NET estática.

Também neste primeiro artigo, afirmei que a estrutura de Navegação permite que você escreva código de Web Forms que faria um aplicativo ASP.NET MVC verde de inveja. No entanto, o aplicativo de pesquisa do exemplo não confirmou essa afirmação, pois o código estava amontoado nos codebehinds e impenetrável para os testes de unidade.

Esclarecerei esse assunto neste segundo artigo, editando o aplicativo de pesquisa de modo que simplesmente fique tão bem estruturado como um aplicativo ASP.NET MVC típico e com um nível maior de capacidade de teste de unidade. Usarei vinculação de dados do ASP.NET padrão junto com a estrutura de Navegação para limpar os codebehinds e extrair a lógica de negócios para uma classe separada, na qual então farei o teste de unidade. Os testes não exigirão nenhuma simulação e a cobertura do código incluirá a lógica de navegação, recursos raramente fornecidos por outras abordagens do teste de unidade do ASP.NET.

Vinculação de dados

O cemitério do código Web Forms está cheio de defuntos inchados de arquivos codebehind, mas não tem de ser dessa maneira. Embora o Web Forms apresentasse a vinculação de dados desde seu início, foi em 2005 que o Visual Studio introduziu os controles de fonte de dados e a sintaxe do Bind para vinculação atualizável bidirecional, permitindo o desenvolvimento de aplicativos Web Forms mais parecidos em estrutura com um aplicativo MVC típico. Os efeitos benéficos de tal código, particularmente com respeito ao teste de unidade, são amplamente reconhecidos, refletidos no fato de que a maior parte do esforço de desenvolvimento do Web Forms para a próxima versão do Visual Studio foi gasta nessa área.

Para demonstrar, tomarei o aplicativo de pesquisa desenvolvido no primeiro artigo e irei convertê-lo em uma arquitetura parecida com MVC. Uma classe de controlador conterá a lógica de negócios e as classes ViewModel conterão os dados para a comunicação entre o controlador e as exibições. Será necessário muito pouco esforço de desenvolvimento, pois o código atualmente nos codebehinds pode ser recortado e colado quase literalmente no controlador.

Começando com Question1.aspx, a primeira etapa é criar uma classe Question ViewModel contendo uma propriedade de cadeia de caracteres de modo que a resposta selecionada possa ser passada de e para o controlador:

public class Question
{
  public string Answer
  {
    get;
    set;
  }
}

A seguir, vem a classe do controlador que chamarei de SurveyController; um objeto Plain Old CLR, diferente do controlador MVC. Question1.aspx precisa de dois métodos, um para a recuperação de dados que retorna a classe Question ViewModel e um para a atualização de dados que aceita a classe Question ViewModel:

public class SurveyController
{
  public Question GetQuestion1()
  {
    return null;
  }
  public void UpdateQuestion1(Question question)
  {
  }
}

Para definir esses métodos usarei o código no codebehind de Question1.aspx, movendo a lógica de carregamento de página para GetQuestion1 e a lógica do manipulador de clique de botão para UpdateQuestion1. Como o controlador não tem acesso aos controles da página, a classe Question ViewModel é usada para obter e definir a resposta em vez da lista de botões de opção. O método GetQuestion1 requer um ajuste adicional para garantir que a resposta padrão retornada seja “Web Forms”:

public Question GetQuestion1()
{
  string answer = "Web Forms";
  if (StateContext.Data["answer"] != null)
  {
    answer = (string)StateContext.Data["answer"];
  }
  return new Question() { Answer = answer };
}

No MVC, a vinculação de dados está no nível da solicitação com a solicitação mapeada para um método de controlador por meio de registro de rota, mas no Web Forms a vinculação de dados está no nível de controle com o mapeamento feito usando um ObjectDataSource. Assim, para vincular Question1.aspx aos métodos SurveyController, adicionarei um FormView conectado a uma fonte de dados apropriadamente configurada.

    <asp:FormView ID="Question" runat="server"
      DataSourceID="QuestionDataSource" DefaultMode="Edit">
      <EditItemTemplate>
      </EditItemTemplate>
    </asp:FormView>
    <asp:ObjectDataSource ID="QuestionDataSource" 
      runat="server" SelectMethod="GetQuestion1" 
      UpdateMethod="UpdateQuestion1" TypeName="Survey.SurveyController"  
      DataObjectTypeName="Survey.Question" />

A etapa final é mover a pergunta, consistindo na lista de botões de opção e botão, dentro de EditItemTemplate do FormView. Ao mesmo tempo, duas alterações devem ser feitas para que o mecanismo de vinculação de dados funcione. A primeira é usar a sintaxe do Bind de modo que a resposta retornada de GetQuestion1 seja exibida e a nova resposta selecionada seja passada de volta a UpdateQuestion1. A segunda é definir o CommandName do botão como Update, assim UpdateQuestion1 será chamado automaticamente quando for pressionado (você observará que o atributo Selected do primeiro item da lista foi removido, pois definir a resposta padrão como “Web Forms” agora é gerenciado em GetQuestion1):

    <asp:RadioButtonList ID="Answer" runat="server"
      SelectedValue='<%# Bind("Answer") %>'>
      <asp:ListItem Text="Web Forms" />
      <asp:ListItem Text="MVC" />
    </asp:RadioButtonList>
    <asp:Button ID="Next" runat="server" 
      Text="Next" CommandName="Update" />

O processo está completo para Question1.aspx e seu codebehind está felizmente vazio. As mesmas etapas podem ser seguidas para adicionar a vinculação de dados a Question2.aspx, mas seu codebehind não pode ser completamente apagado porque o código de carregamento de página relacionado ao hiperlink de navegação regressiva deve permanecer aí por enquanto. Na próxima seção, em que a integração da estrutura de Navegação com a vinculação de dados é discutida, ela será movida para a marcação e o codebehind evacuado.

Adicionar a vinculação de dados a Thanks.aspx é similar, mas em vez de reutilizar a classe Question ViewModel denominada de forma imprópria, criarei uma nova chamada de Summary com uma propriedade de cadeia de caracteres para conter as respostas selecionadas:

public class Summary
{
  public string Text
  {
    get;
    set;
  }
}

Como Thanks.aspx é uma tela somente leitura, apenas um método de recuperação de dados é necessário no controlador e, como com Question2.aspx, todo o código de carregamento de página além da lógica de navegação regressiva pode ser movido para esse método:

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

Como nenhuma funcionalidade de atualização é necessária, o ItemTemplate de FormView é usado em vez de EditItemTemplate, e a sintaxe para a vinculação unidirecional, Eval, é usada no lugar de Bind:

    <asp:FormView ID="Summary" runat="server" 
      DataSourceID="SummaryDataSource">
      <ItemTemplate>
        <asp:Label ID="Details" runat="server" 
          Text='<%# Eval("Text") %>' />
      </ItemTemplate>
    </asp:FormView>
    <asp:ObjectDataSource ID="SummaryDataSource" 
      runat="server"
      SelectMethod="GetSummary" 
      TypeName="Survey.SurveyController" />

Metade da batalha do teste de unidade está vencida, pois a lógica de negócios do aplicativo de pesquisa foi extraída para uma classe separada. No entanto, como o código foi colado no controlador virtualmente inalterado do codebehind, a potência da vinculação de dados ainda não está totalmente utilizada.

Vinculação de dados de navegação

O código do aplicativo de pesquisa ainda tem alguns problemas: Somente os métodos de atualização no SurveyController deve conter lógica de navegação, e os codebehinds não estão vazios. O teste de unidade não deve começar até que esses problemas estejam resolvidos, pois o primeiro resultaria em testes de unidade desnecessariamente complexos e o último impediria 100% da cobertura do teste de unidade.

Os parâmetros de seleção dos controles de fonte de dados tornam o acesso ao objeto HttpRequest em métodos vinculados a dados redundante. Por exemplo, a classe QueryStringParameter permite que dados de cadeia de caracteres de consulta sejam passados como parâmetros para métodos vinculados a dados. A estrutura de Navegação tem uma classe NavigationDataParameter que executa o trabalho equivalente para os dados de estado no objeto StateContext.

Equipado com esse NavigationDataParameter, posso voltar a GetQuestion1, removendo todo o código que acessa os dados de estado tornando a resposta, em vez disso, um parâmetro do método. Isso simplifica de forma significativa o código:

public Question GetQuestion1(string answer)
{
  return new Question() { Answer = answer ?? "Web Forms" };
}

A alteração conjunta em Question1.aspx é adicionar o NavigationDataParameter à sua fonte de dados. Isso envolve registrar primeiro o namespace Navigation na parte superior da página:

    <%@ Register assembly="Navigation" 
                           namespace="Navigation" 
                            tagprefix="nav" %>

O NavigationDataParameter pode então ser adicionado aos parâmetros de seleção da fonte de dados.

    <asp:ObjectDataSource ID="QuestionDataSource" runat="server"
      SelectMethod="GetQuestion1" UpdateMethod="UpdateQuestion1" 
      TypeName="Survey.SurveyController" 
      DataObjectTypeName="Survey.Question" >
      <SelectParameters>
        <nav:NavigationDataParameter Name="answer" />
      </SelectParameters>
    </asp:ObjectDataSource>

O método GetQuestion1, tendo sido retirado dele todo o código específico da Web, pode agora facilmente passar por um teste de unidade. O mesmo pode ser feito para GetQuestion2.

Para o método GetSummary, dois parâmetros são necessários, um para cada resposta. O segundo parâmetro é um bool para corresponder como os dados são passados por UpdateQuestion2, e deve ser anulável, pois a segunda pergunta não é sempre perguntada:

public Summary GetSummary(string technology, bool? navigation)
{
  Summary summary = new Summary();
  summary.Text = technology;
  if (navigation.HasValue)
  {
    summary.Text += ", " + navigation.Value;
  }
  return summary;
}

E a alteração correspondente na fonte de dados em Thanks.aspx é a adição dos dois NavigationDataParameters:

    <asp:ObjectDataSource ID="SummaryDataSource" runat="server"
      SelectMethod="GetSummary" TypeName="Survey.SurveyController" >
      <SelectParameters>
        <nav:NavigationDataParameter Name="technology" />
        <nav:NavigationDataParameter Name="navigation" />
      </SelectParameters>
    </asp:ObjectDataSource>

O primeiro problema com o código do aplicativo de pesquisa foi resolvido, porque agora somente os métodos de atualização no controlador contêm lógica de navegação.

Você se lembrará de que a estrutura de Navegação melhora a funcionalidade de navegação estrutural estática fornecida pelo mapa de site do Web Forms, controlando os estados visitados junto com seus dados de estado e criando uma trilha de navegação estrutural contextual da rota exata tomada pelo usuário. Para construir hiperlinks de navegação regressiva na marcação, sem precisar de codebehind, a estrutura de Navegação fornece um CrumbTrailDataSource análogo ao controle SiteMapPath. Quando usado como a fonte de dados existente de um ListView, a CrumbTrailDataSource retorna uma lista de itens, um por estado anteriormente visitado, com cada um contendo uma URL NavigationLink que permite navegação contextual de volta ao estado.

Usarei essa nova fonte de dados para mover a navegação regressiva do Question2.aspx para sua marcação. Primeiro, adicionarei um ListView conectado à CrumbTrailDataSource:

    <asp:ListView ID="Crumbs" runat="server" 
      DataSourceID="CrumbTrailDataSource">
      <LayoutTemplate>
        <asp:PlaceHolder ID="itemPlaceholder" runat="server" />
      </LayoutTemplate>
      <ItemTemplate>
      </ItemTemplate>
    </asp:ListView>
    <nav:CrumbTrailDataSource ID="CrumbTrailDataSource" runat="server" />

Em seguida, excluirei o código de carregamento de página do codebehind de Question2.aspx, moverei o hiperlink de navegação regressiva para dentro do ItemTemplate do ListView e usarei a vinculação Eval para preencher a propriedade NavigateUrl:

    <asp:HyperLink ID="Question1" runat="server"
      NavigateUrl='<%# Eval("NavigationLink") %>' Text="Question 1"/>

Você notará que a propriedade Text de HyperLink está codificada em “Question 1.” Isso funciona muito bem para Question2.aspx, pois a única navegação regressiva possível é para a primeira pergunta. No entanto, o mesmo não pode ser dito para Thanks.aspx, pois é possível retornar à primeira ou segunda perguntas. Felizmente, a configuração de navegação inserida no arquivo StateInfo.config permite que um atributo de título seja associado a cada estado, como:

    <state key="Question1" page="~/Question1.aspx" title="Question 1">

E então a CrumbTrailDataSource disponibiliza esse título para vinculação de dados:

    <asp:HyperLink ID="Question1" runat="server"
      NavigateUrl='<%# Eval("NavigationLink") %>' 
      Text='<%# Eval("Title") %>'/>

Aplicar essas mesmas alterações a Thanks.aspx resolve o segundo problema com o código do aplicativo de pesquisa, pois todos os codebehinds estão agora vazios. No entanto, todo esse esforço será em vão se o SurveyController não puder passar por teste de unidade.

Teste de unidade

Com o aplicativo de pesquisa agora perfeitamente estruturado, os codebehinds estão vazios e toda a lógica da interface do usuário está na marcação da página, é hora de fazer o teste de unidade da classe SurveyController. Os métodos de recuperação de dados GetQuestion1, GetQuestion2 e GetSummary claramente podem passar pelo teste de unidade, pois não contêm nenhum código específico da Web. Somente os métodos UpdateQuestion1 e Update­Question2 apresentam um desafio ao teste de unidade. Sem a estrutura de Navegação, esses dois métodos conteriam chamadas de roteamento e redirecionamento, a tradicional maneira de mover e passar dados entre páginas ASPX, ambas as quais enviam exceções quando usadas fora de um ambiente da Web, causando uma falha no teste de unidade no primeiro obstáculo. No entanto, com a estrutura de Navegação em funcionamento, esses dois métodos podem passar completamente pelo teste de unidade sem necessitar de qualquer alteração de código ou objetos fictícios.

Para iniciantes, criarei um projeto Unit Test para a pesquisa. Clicar com o botão direito do mouse dentro de qualquer método da classe SurveyController e selecionar a opção de menu “Create Unit Tests ...” criará um projeto com as referências necessárias incluídas e uma classe Survey­ControllerTest.

Você irá se lembrar de que a estrutura de Navegação precisa que a lista de estados e transições seja configurada no arquivo StateInfo.config. Para que o projeto do teste de unidade use a mesma configuração de navegação, o arquivo StateInfo.config do projeto da Web deve ser implantado quando os testes de unidade forem executados. Com isso em mente, clicarei duas vezes no item de solução Local.testsettings e selecionarei a caixa de seleção “Enable deployment” na guia Deployment. Em seguida, decorarei a classe SurveyControllerTest com o atributo DeploymentItem fazendo referência a esse arquivo StateInfo.config:

[TestClass]
[DeploymentItem(@"Survey\StateInfo.config")]
public class SurveyControllerTest
{
}

A seguir, um arquivo app.config deve ser adicionado ao projeto de teste apontando para esse arquivo StateInfo.config implantado (essa configuração também é necessária ao projeto da Web, mas foi automaticamente adicionado pela instalação do NuGet):

<configuration>
  <configSections>
    <sectionGroup name="Navigation">
      <section name="StateInfo" type=
        "Navigation.StateInfoSectionHandler, Navigation" />
    </sectionGroup>
  </configSections>
  <Navigation>
    <StateInfo configSource="StateInfo.config" />
  </Navigation>
</configuration>

Com essa configuração instalada, o teste de unidade pode começar. Seguirei o padrão AAA para estruturar um teste de unidade:

  1. Arranjar: configurar as pré-condições e os dados de teste.
  2. Atuar: executar a unidade sob teste.
  3. Assegurar: verificar o resultado.

Começando com o método UpdateQuestion1, mostrarei o que é necessário em cada uma dessas três etapas quando se trata de testar a navegação e a passagem de dados na estrutura de Navegação.

A etapa Arranjar configura o teste de unidade, criando o objeto sob teste e os parâmetros que precisam ser passados para o método sendo testado. Para UpdateQuestion1, isso significa criar um SurveyController e um Question preenchido com a resposta relevante. No entanto, uma condição de configuração de navegação extra é necessária, espelhando a navegação que ocorre quando o aplicativo da Web é iniciado. Quando o aplicativo da Web de pesquisa é iniciado, a estrutura de Navegação intercepta a solicitação da página de inicialização, Question1.aspx, e navega para a caixa de diálogo cujo atributo de caminho corresponde a essa solicitação no arquivo StateInfo.config:

    <dialog key="Survey" initial="Question1" path="~/Question1.aspx">

Navegar usando uma chave de caixa de diálogo vai para o estado mencionado em seu atributo inicial, assim o estado Question1 é alcançado. Como não é possível definir uma página de inicialização em um teste de unidade, essa navegação de caixa de diálogo deve ser executada manualmente e é a condição extra requerida na etapa Arranjar:

StateController.Navigate("Survey");

A etapa Atuar chama o método sob teste. Isso simplesmente envolve passar o Question com sua resposta preenchida para UpdateQuestion1 e, portanto, não precisa de qualquer detalhe específico de navegação.

A etapa Assegurar compara os resultados em relação aos valores esperados. Verificar os resultados da navegação e da passagem dos dados pode ser feito usando as classes da estrutura de Navegação. Você se lembrará de que o StateContext fornece acesso aos dados de estado por meio de sua propriedade Data, que é inicializada com o NavigationData passado durante a navegação. Isso pode ser usado para verificar se UpdateQuestion1 passa a resposta selecionada para o próximo estado. Assim, assumindo que “Web Forms” seja passado para o método, o Assegurar torna-se:

Assert.AreEqual("Web Forms", (string) StateContext.Data["technology"]);

O StateContext também tem uma propriedade State que controla o estado atual. Isso pode ser usado para verificar se uma navegação ocorreu como esperado, por exemplo, que passar “Web Forms” para UpdateQuestion1 deve navegar para Question2:

Assert.AreEqual("Question2", StateContext.State.Key);

Enquanto o StateContext contém detalhes sobre o estado atual e os dados associados, a Crumb é a classe equivalente de estados anteriormente visitados e seus dados, chamado dessa forma porque a cada vez que um usuário navega, um novo é adicionado à trilha de navegação estrutural. Essa trilha de navegação estrutural ou lista de trilhas está acessível via a propriedade Crumbs do StateController (e são os dados de backup da CrumbTrailDataSource da seção anterior). Preciso recorrer à essa lista para verificar se UpdateQuestion1 armazena a resposta passada em seus dados de estado antes de navegar, pois ao ocorrer a navegação, uma trilha é criada contendo esses dados de estado. Supondo que a resposta passada esteja em “Web Forms,” os dados na primeira e única trilha podem ser verificados:

Assert.AreEqual("Web Forms", (string) StateController.Crumbs[0].Data["answer"]);

O padrão AAA de escrever um teste de unidade estruturado foi coberto em relação à estrutura de Navegação. Reunindo todas essas etapas, a seguir está um teste de unidade para verificar se o estado Question2 é alcançado depois de passar uma resposta de “Web Forms” para UpdateQuestion1 (com uma linha em branco inserida entre as diferentes etapas para maior clareza):

[TestMethod]
public void UpdateQuestion1NavigatesToQuestion2IfAnswerIsWebForms()
{
  StateController.Navigate("Survey");
  SurveyController controller = new SurveyController();
  Question question = new Question() { Answer = "Web Forms" };
  controller.UpdateQuestion1(question);
  Assert.AreEqual("Question2", StateContext.State.Key);
}

Embora isso seja tudo de que você precisa para poder fazer um teste de unidade dos diferentes conceitos da estrutura de Navegação, vale a pena continuar com UpdateQuestion2 porque ele tem algumas diferenças em suas etapas Arranjar e Atuar. A condição de navegação requerida em sua etapa Arranjar é diferente porque, para chamar UpdateQuestion2, o estado atual deve ser Question2 e os dados do estado atual devem conter a resposta de tecnologia “Web Forms”. No aplicativo da Web essa navegação e passagem de dados são gerenciadas pela interface do usuário porque o usuário não pode avançar para a segunda pergunta sem responder “Web Forms” à primeira pergunta. No entanto, no ambiente do teste de unidade, isso deve ser feito manualmente. Isso envolve a mesma navegação de caixa de diálogo exigida por UpdateQuestion1 para alcançar o estado Question1, seguida de uma navegação que passa a transition key Next e a resposta “Web Forms” em NavigationData:

StateController.Navigate("Survey");
StateController.Navigate(
  "Next", new NavigationData() { { "technology", "Web Forms" } });

A única diferença na etapa Assegurar de UpdateQuestion2 vem quando a verificação de sua resposta é armazenada nos dados de estado antes da navegação. Quando essa verificação foi feita para UpdateQuestion1, a primeira trilha na lista foi usada porque apenas um estado foi visitado, ou seja Question1. No entanto, para UpdateQuestion2, haverá duas trilhas na lista porque Question1 e Question2 foram alcançadas. As trilhas aparecem na lista na ordem em que foram visitadas, assim Question2 é a segunda entrada e a verificação de requisito se torna:

Assert.AreEqual("Yes", (string)StateController.Crumbs[1].Data["answer"]);

A elevada ambição de um código de Web Forms que passe por teste de unidade foi obtida. Isso foi feito usando vinculação de dados padrão com ajuda da estrutura de Navegação. É menos prescritivo do que outras abordagens de teste de unidade de ASP.NET, pois o controlador não teve de herdar ou implementar qualquer classe ou interface de estrutura, e seus métodos não tiveram de retornar qualquer tipo de estrutura específico.

O MVC já está com inveja?

O MVC deve estar sentindo pontadas de inveja, pois o aplicativo de pesquisa é tão bem estruturado quanto um aplicativo MVC típico, mas tem um nível maior de teste de unidade. O código de navegação do aplicativo de pesquisa aparece dentro dos métodos do controlador e é testado junto com o resto da lógica de negócios. Em um aplicativo MVC, o código de navegação não é testado, porque ele está contido dentro dos tipos de retorno dos métodos do controlador, como o RedirectResult. No meu próximo artigo sobre a estrutura de Navegação, aumentarei a inveja do MVC criando um aplicativo de página única e compatível com a otimização do mecanismo de pesquisa com aderência aos princípios de DRY, Não ser repetitivo, difíceis de se obter em seu equivalente MVC.

Dito isso, a vinculação de dados do Web Forms tem problemas que não estão presentes em seu equivalente MVC. Por exemplo, é difícil usar injeção de dependência nas classes do controlador, e tipos aninhados nas classes ViewModel não têm suporte. Mas Web Forms aprendeu muito do MVC e a próxima versão do Visual Studio verá uma experiência de vinculação de dados do Web Forms altamente aprimorada.

Há muito mais na integração da estrutura de navegação com a vinculação de dados do que foi mostrado aqui. Por exemplo, há um controle DataPager que, diferente do DataPager do ASP.NET, não precisa ser conectado a um controle ou requer um método de contagem separado. Se estiver interessado em descobrir mais, uma documentação abrangente e um exemplo de código estão disponíveis 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 Navegação para ASP.NET Web Forms, 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: Scott Hanselman