Compartir a través de


ASP.NET

Pruebas unitarias en el marco Navigation for ASP.NET Web Forms

Graham Mendick

Descargar el ejemplo de código

Descargue la Biblioteca

El marco Navigation for ASP.NET Web Forms, un proyecto de código abierto hospedado en navigation.codeplex.com, abre nuevas posibilidades para escribir aplicaciones Web Forms, gracias a un nuevo enfoque en la navegación y la transmisión de datos. En el código Web Forms tradicional, la manera de transmitir los datos depende de la navegación realizada. Se puede conservar, por ejemplo, en la cadena de consulta o en los datos de la ruta durante una redirección, pero en los valores de control o el estado de la vista durante una devolución de datos. En el marco Navigation for ASP.NET Web Forms (de aquí en adelante “marco Navigation” por motivos de brevedad), sin embargo, se emplea un solo origen de datos para todas las situaciones.

En mi primer artículo (msdn.microsoft.com/magazine/hh975349) presenté el marco Navigation y creé un ejemplo de aplicación de encuesta en línea para ilustrar algunos de los conceptos fundamentales y las ventajas que otorga. Específicamente, mostré cómo nos brinda la posibilidad de generar un conjunto de hipervínculos de navegación contextuales dinámicos, que permiten que el usuario pueda volver a preguntas visitadas previamente, con las respuestas del usuario restauradas. Así superamos las limitaciones de la funcionalidad del mapa del sitio ASP.NET estático.

Además, en ese primer artículo, aseveré que el marco Navigation nos permitía escribir código Web Forms que causaría que las aplicaciones ASP.NET MVC se pusieran verdes de envidia. La aplicación de encuesta en línea de ejemplo, sin embargo, no respaldó esa afirmación, ya que el código estaba apiñado en el código subyacente y no quedaba al alcance de las pruebas unitarias.

Dejaré las cosas en claro en este segundo artículo, donde editaré la aplicación de encuesta para que quede estructurada correctamente como una aplicación ASP.NET MVC típica y para dejarla más accesible a las pruebas unitarias. Usaré el enlace de datos ASP.NET estándar junto con el marco Navigation para vaciar los códigos subyacentes y extraer la lógica de negocios en una clase separada, que luego usaré en las pruebas unitarias. Estas pruebas no requerirán de ficciones y el código cubrirá también la lógica de navegación, una característica que rara vez se encuentra en los otros enfoques de pruebas de unidad en ASP.NET.

Enlace de datos

El cementerio del código Web Forms está lleno de los cadáveres hinchados de los archivos de código subyacente, pero no tiene por qué que ser así. Aunque Web Forms cuenta con el enlace de datos desde su inauguración, fue en 2005 que Visual Studio presentó los controles de origen de datos y la sintaxis Bind para realizar enlaces actualizables bidireccionales, lo que permite el desarrollo de aplicaciones Web Forms con una estructura más parecida a las aplicaciones MVC típicas. Los efectos beneficiosos de este tipo de código, especialmente con respecto a las pruebas unitarias, son ampliamente reconocidos, lo que se ve reflejado en el hecho que la mayor parte del desarrollo de Web Forms para la siguiente versión de Visual Studio se realizó en esta área.

Para demostrarlo, tomaré la aplicación de encuesta que desarrollé en el primer artículo y la convertiré en una estructura similar a MVC. Una clase de control contendrá la lógica de negocios y las clases ViewModel almacenarán los datos para la comunicación entre el controlador y las vistas. El trabajo de desarrollo necesario será muy reducido, ya que podremos cortar y pegar el código que actualmente se encuentra en los códigos subyacentes casi literalmente en el controlador.

Al comenzar con Question1.aspx, lo primero que debemos hacer es crear una clase ViewModel llamada Question que contenga una propiedad de cadena, de modo que la respuesta seleccionada pueda pasar al controlador y desde él:

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

Luego viene la clase controladora, que llamaré SurveyController; un objeto CLR tradicional, a diferencia de un controlador MVC. Question1.aspx necesita dos métodos, uno para la recuperación de los datos que devuelve la clase ViewModel llamada Question y otro para la actualización de los datos que acepta la clase ViewModel Question:

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

Para completar estos métodos usaré el código subyacente de Question1.aspx: trasladaré la lógica de carga de página en GetQuestion1 y la lógica del controlador de clics de botón en UpdateQuestion1. Como el controlador no tiene acceso a los controles de la página, usamos la clase ViewModel Question para obtener y establecer la pregunta en vez de la lista de botones de opción. El método GetQuestion1 requiere otra modificación más para garantizar que se devuelva “Web Forms” como respuesta predeterminada:

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

En MVC; el enlace de datos se realiza en el nivel de la solicitud y la solicitud se asigna a un método de control mediante el registro de la ruta, pero en Web Forms el enlace de datos se encuentra en el nivel de control y la asignación se realiza con un ObjectDataSource. Por lo tanto, para conectar Question1.aspx con los métodos SurveyController, agregaré un FormView conectado a un origen de datos debidamente configurado:

<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" />

El último paso es trasladar la pregunta, formada por la lista de botones de opción y el botón, al interior de EditItemTemplate de FormView. Al mismo tiempo, debemos realizar dos cambios para que funcione el mecanismo de enlace de datos. El primero es usar la sintaxis Bind para que se muestre la respuesta devuelta por GetQuestion1 y la respuesta recién seleccionada se vuelva a pasar a UpdateQuestion1. El segundo es establecer el CommandName del botón en Update, para que al presionarlo se llame automáticamente UpdateQuestion1 (notará que eliminamos el atributo Selected del primer elemento de la lista, ya que GetQuestion1 ahora establece la respuesta predeterminada en “Web Forms”):

<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" />

El proceso está completo para Question1.aspx y, gratamente, el código subyacente está vacío. Podemos seguir los mismos pasos para agregar el enlace de datos a Question2.aspx, pero no podemos borrar el código subyacente completamente, ya que por el momento el código de carga de página relacionado con el hipervínculo de navegación hacia atrás debe quedarse allí. En la siguiente sección, que trata sobre la integración del marco Navigation con el enlace de datos, lo trasladaremos al marcado, para dejar vacío el código subyacente.

La adición del enlace de datos a Thanks.aspx es parecido, pero en vez de volver a usar la clase ViewModel llamada (de manera poco apropiada) Question, crearé una nueva clase llamada Summary con una propiedad de cadena para contener las respuestas seleccionadas:

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

Como Thanks.aspx es una pantalla de solo lectura, solo necesitamos un método de recuperación de datos en el controlador e, igual que con Question2.aspx, podemos trasladar todo el código de carga de página (aparte de la lógica de navegación hacia atrás) a este 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 no necesitamos ninguna funcionalidad de actualización, usamos el ItemTemplate llamado FormView en lugar de EditItemTemplate, y empleamos la sintaxis de enlace unidireccional, Eval, en 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" />

Ya ganamos la mitad de la batalla de las pruebas unitarias, porque ya extrajimos la lógica de negocios de la aplicación de encuesta a una clase independiente. Pero como copiamos el código del código subyacente al controlador prácticamente sin cambios, no estamos aprovechando todo el potencial del enlace de datos.

Enlace de datos de navegación

El código de la aplicación de encuesta todavía tiene dos problemas: solo los métodos de actualización de SurveyController deberían contener lógica de navegación y los códigos subyacentes no están vacíos. No debemos comenzar con las pruebas unitarias antes de solucionar estos problemas, ya que la primera situación conduciría a pruebas unitarias innecesariamente complejas para los métodos Get y la segunda situación evitaría las pruebas unitarias en un 100 por ciento.

Los parámetros select de los controles de origen de datos provocan que el acceso al objeto HttpRequest en los métodos enlazados por datos sea redundante. La clase QueryStringParameter, por ejemplo, permite pasar los datos de cadena de consulta como parámetros a los métodos enlazados por datos. El marco Navigation tiene una clase NavigationDataParameter que realiza el trabajo equivalente para los datos de estado en el objeto StateContext.

Equipado con este NavigationDataParameter, puedo volver a revisar GetQuestion1 y eliminar todo el código que accede a los datos de estado, al convertir la respuesta en un parámetro de método. Esto simplifica el código de manera significativa:

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

El cambio complementario en Question1.aspx radica en agregar NavigationDataParameter al origen de datos. Para esto debemos registrar el espacio de nombre Navigation primero en la parte superior de la página:

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

Luego podemos agregar NavigationDataParameter a los parámetros select del origen de datos.

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

Una vez que eliminamos todo el código relacionado con la web de GetQuestion1, nos resulta fácil realizar las pruebas unitarias. Lo mismo podemos hacer con GetQuestion2.

Para el método GetSummary necesitamos dos parámetros, uno para cada respuesta. El segundo parámetro es un valor booleano que indica la forma que tienen los datos provenientes de UpdateQuestion2 y debe aceptar valores Null, ya que la segunda pregunta no se realiza siempre:

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

Y el cambio correspondiente en origen de datos en Thanks.aspx es la adición de los dos NavigationDataParameter:

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

Abordamos el primer problema del código de aplicación de encuesta, ya que ahora solo los métodos de actualización del controlador contienen lógica de navegación.

Recordará que el marco Navigation mejora la funcionalidad de la ruta de navegación estática provista por el mapa del sitio de Web Forms: realiza un seguimiento de los estados visitados, junto con los datos de estado, y crea una ruta de navegación contextual de la ruta que el usuario tomó en la práctica. Para construir los hipervínculos de navegación hacia atrás en marcado —sin usar código subyacente— el marco Navigation brinda un CrumbTrailDataSource análogo al control de SiteMapPath. Al usarlo como origen de datos de respaldo para ListView, CrumbTrailDataSource devuelve una lista de elementos, uno por cada estado visitado previamente, donde cada uno cuenta con una dirección URL, NavigationLink, que permite la navegación contextual a ese estado.

Usaré este nuevo origen de datos para trasladar la navegación hacia atrás de Question2.aspx al marcado. Primero, agregaré un ListView conectado a 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" />

Luego eliminaré el código de la carga de página del código subyacente de Question2.aspx, desplazaré el hipervínculo de navegación hacia atrás dentro del ItemTemplate ListView y usaré el enlace Eval para rellenar la propiedad NavigateUrl:

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

Verá que la propiedad Text de HyperLink está establecida de forma rígida en “Question 1”. Esto funciona perfectamente para Question2.aspx, porque la única navegación hacia atrás posible es a la primera pregunta. No podemos decir lo mismo de Thanks.aspx, sin embargo, ya que en este caso se puede volver tanto a la primera como a la segunda pregunta. Afortunadamente, la configuración de navegación en el archivo StateInfo.config nos permite asociar el atributo title a cada estado; por ejemplo:

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

Y luego CrumbTrailDataSource pone este título a disposición del enlace de datos:

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

El segundo problema de la aplicación de encuesta lo solucionamos al aplicar estos mismos cambios a Thanks.aspx, ya que ahora todos los códigos subyacentes están vacíos. Sin embargo, todo este esfuerzo será en vano si no podemos realizar las pruebas unitarias para SurveyController.

Pruebas unitarias

Ahora que la aplicación de encuesta está bien estructurada —con los códigos subyacentes vacíos y toda la lógica de la interfaz de usuario en el marcado de página—, es hora de escribir las pruebas unitarias para la clase SurveyController. No encontramos dificultades en los métodos de recuperación de datos GetQuestion1, GetQuestion2 y GetSummary, ya que no contienen código relacionado con la web. Solo los métodos UpdateQuestion1 y Update­Question2 presentan dificultades para la pruebas unitarias. Sin el marco Navigation, estos dos métodos contendrían llamadas de enrutamiento y redirección —la forma tradicional de mover y traspasar datos entre las páginas ASPX— que generan excepciones al usarlos fuera de un entorno web, lo que presenta un primer obstáculo para las pruebas unitarias. Pero al haber establecido correctamente el marco Navigation, podemos realizar un conjunto completo de pruebas unitarias para estos dos métodos sin necesidad de cambios de código ni objetos ficticios.

Para comenzar, crearé un proyecto de pruebas unitarias para la encuesta. Al hacer clic con el botón secundario dentro de cualquier método de la clase SurveyController y seleccionar la opción del menú “Crear pruebas unitarias...”, podemos crear un proyecto con las referencias necesarias y una clase Survey­ControllerTest.

Recordará que el marco Navigation requiere que la lista de estados y transiciones esté configurada en el archivo StateInfo.config. Para que el proyecto de pruebas unitarias use esta misma configuración de navegación, debemos implementar el archivo StateInfo.config del proyecto web al ejecutar las pruebas unitarias. Teniendo esto en mente, haré doble clic en el elemento de solución Local.testsettings y seleccionaré la casilla “Habilitar implementación” en la pestaña Implementación. Luego decoraré la clase SurveyControllerTest con el atributo DeploymentItem, que hace referencia al archivo StateInfo.config:

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

Luego debemos agregar un archivo app.config al proyecto de prueba, que apunte al archivo implementado StateInfo.config (el proyecto web también necesita esta configuración, pero la instalación NuGet la agregó automáticamente).

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

Ahora que tenemos esta configuración, podemos comenzar con las pruebas unitarias. Seguiré el patrón AAA para estructurar las pruebas unitarias:

  1. Acomodar: configurar las condiciones preliminares y los datos de las pruebas.
  2. Actuar: ejecutar la unidad sometida a prueba.
  3. Aprobar: comprobar el resultado.

Comenzando por el método UpdateQuestion1, mostraré lo que necesitamos en cada uno de estos tres pasos cuando se trata de probar la navegación y la transmisión de datos en el marco Navigation.

El paso Acomodar configura la prueba unitaria: crea el objeto sometido a prueba y los parámetros que transmitimos al método sometido a prueba. En el caso de UpdateQuestion1, esto significa crear un SurveyController y una Question con la respuesta pertinente. Sin embargo, necesitamos una condición de configuración de navegación adicional que refleje la navegación que ocurre al iniciar la aplicación web. Cuando se inicia la aplicación web de encuesta, el marco Navigation intercepta la solicitud para la página de inicio, Question1.aspx, y navega al cuadro de diálogo con el atributo de ruta que coincida con esta solicitud en el archivo StateInfo.config:

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

Al navegar mediante la clave de cuadro de un diálogo llegamos al estado mencionado en el atributo inicial, de modo que alcanzamos el estado Question1. Como en las pruebas unitarias no podemos establecer una página de inicio, debemos realizar esta navegación de cuadro de diálogo en forma manual; esta es la condición adicional requerida en el paso Acomodar:

StateController.Navigate("Survey");

El paso Actuar llama el método sometido a prueba. Esto implica simplemente transmitir la Question con la respuesta rellena a UpdateQuestion1 y, por ende, no requiere detalles de navegación específicos.

El paso Aprobar compara los resultados con los valores esperados. La comprobación de los resultados de la navegación y transmisión de los datos las podemos realizar mediante las clases del marco Navigation. Recordará que StateContext provee acceso a los datos de estado a través de la propiedad Data, que se inicializa con los NavigationData que pasamos durante la navegación. Esto nos sirve para comprobar que UpdateQuestion1 transmita la respuesta seleccionada al estado siguiente. Entonces, suponiendo que “Web Forms” se pase al método, Aprobar se convierte en:

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

StateContext también tiene una propiedad State que realiza un seguimiento del estado actual. Esta nos permite comprobar si la navegación ocurrió de la manera esperada, por ejemplo, que al pasar “Web Forms” a UpdateQuestion1 se navegue a Question2:

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

Mientras StateContext contiene los detalles sobre el estado actual y los datos asociados, Crumb (“miga”) es la clase equivalente para los estados previos y sus datos. Recibe este nombre, ya que durante la navegación el usuario deja una huella, como si fuera tirando “migas de pan”. Esta ruta de navegación o lista de “crumbs” se puede acceder través de la propiedad Crumbs de StateController (y son los datos de respaldo de CrumbTrailDataSource de la sección anterior). Debo recurrir a esta lista para comprobar que UpdateQuestion1 almacene la respuesta transmitida en los datos de estado antes de navegar, ya que una vez que ocurre la navegación, se crea una “crumb” que aloja estos datos de estado. Si suponemos que la respuesta transmitida es “Web Forms”, podemos comprobar los datos de la primera y única “crumb”:

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

Contemplamos el patrón AAA para escribir pruebas unitarias estructuradas en el marco Framework. Al combinar todos estos pasos, a continuación vemos una prueba unitaria para confirmar que se alcance el estado Question2 después de transmitir la respuesta “Web Forms” a UpdateQuestion1 (con una línea en blanco entre los diferentes pasos, para mayor claridad).

[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);
}

Aunque eso es todo lo que necesitamos para realizar las pruebas unitarias de los diferentes conceptos del marco Navigation, vale la pena seguir con UpdateQuestion2, ya que presenta algunas diferencias en los pasos Acomodar y Actuar. La condición de navegación requerida en el paso Acomodar es diferente ya que, para llamar UpdateQuestion2, el estado actual debe ser Question2 y los datos de estado actual deben contener la respuesta “Web Forms” para la tecnología. En la aplicación web, la interfaz de usuario se encarga de esta navegación y transmisión de datos, ya que el usuario no puede avanzar a la segunda pregunta sin responder “Web Forms” en la primera pregunta. En el entorno de las pruebas unitarias, sin embargo, debemos realizarlo manualmente. Esto implica la misma navegación de cuadro de diálogo requerida por UpdateQuestion1 para alcanzar el estado Question1, seguido por una navegación que pasa la clave de transición Next y la respuesta “Web Forms” en NavigationData:

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

La única diferencia en el paso Aprobar para UpdateQuestion2 lo observamos al comprobar que la respuesta se haya almacenado en los datos de estado antes de la navegación. En el caso de UpdateQuestion1, simplemente usamos la primera “crumb” de la lista, ya que solo habíamos visitado un estado, a saber, Question1. Sin embargo, para UpdateQuestion2, habrá dos “crumbs” en la lista, ya que se alcanzaron tanto Question1 como Question2. Las “crumbs” aparecen en la lista según el orden de recorrido y, por ende, Question2 es la segunda entrada y la comprobación de los requisitos se convierte en:

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

Conseguimos la meta ambiciosa de lograr que el código Web Forms se pudiera someter a pruebas unitarias. Para esto usamos el enlace de datos convencional con la ayuda del marco Navigation. Resulta menos preceptivo que otros sistemas de pruebas unitarias para ASP.NET, ya que el controlador no debe heredar ni implementar ninguna clase de marco o interfaz, y los métodos deben devolver ningún tipo definido en un marco.

¿Ya está celoso MVC?

Ahora sí que MVC debería sentir ataques de celos, ya que la aplicación de encuesta está tan bien estructurada como una aplicación MVC típica, pero las pruebas unitarias se realizan a un nivel más elevado. El código de navegación de la aplicación de encuesta aparece dentro de los métodos del controlador y las pruebas se realizan junto con el resto de la lógica de negocios. En las aplicaciones MVC, no se realizan pruebas del código de navegación, ya que está contenido dentro de los tipos de devolución de los métodos del controlador como por ejemplo RedirectResult. En el siguiente artículo sobre el marco Navigation, entregaré más motivos de celos a MVC, al crear una aplicación de una página optimizada para los motores de búsqueda y que se adhiere a los principios del “No te repitas” (DRY), lo que cuesta mucho lograr con MVC.

Dicho eso, el enlace de datos de Web Forms sí tiene problemas que no están presentes en la contraparte de MVC. Por ejemplo, es difícil usar la inserción de dependencias en las clases de los controladores, y no se permiten los tipos anidados en las clases ViewModel. Pero Web Forms ha aprendido mucho de MVC y la próxima versión de Visual Studio entregará una experiencia enormemente mejorada del enlace de datos en Web Forms.

Hay mucho más para mostrar sobre la integración del marco Navigation con el enlace de datos. Por ejemplo, existe un control localizador de datos que, a diferencia de DataPager de ASP.NET no necesita conectarse a un control ni requiere un método de recuento separado. Si desea obtener más información, encontrará una documentación exhaustiva y código de ejemplo en navigation.codeplex.com.

Graham Mendick es el máximo admirador de Web Forms y pretende demostrar que este puede tener la misma integridad arquitectónica que ASP.NET MVC. Es autor del marco Navigation for ASP.NET Web Forms, que en su opinión, cuando se usa junto con el enlace de datos, puede revitalizar Web Forms.

Gracias al siguiente experto técnico por su ayuda en la revisión de este artículo: Scott Hanselman