Pasar página por grandes cantidades de datos de forma eficaz (C#)

por Scott Mitchell

Descargar PDF

La opción de paginación predeterminada de un control de presentación de datos no es adecuada al trabajar con grandes cantidades de datos, ya que su control de origen de datos subyacente recupera todos los registros, aunque solo se muestre un subconjunto de datos. En tales circunstancias, se debe recurrir a la paginación personalizada.

Introducción

Como se ha explicado en el tutorial anterior, la paginación se puede implementar de una de estas dos maneras:

  • La paginación predeterminada se puede implementar simplemente al activarla opción Habilitar paginación en la etiqueta inteligente del control web de datos; pero siempre que vea una página de datos, ObjectDataSource recupera todos los registros, aunque solo se muestre un subconjunto de ellos en la página
  • La paginación personalizada mejora el rendimiento de la paginación predeterminada y solo recupera los registros de la base de datos que deben mostrarse para la página concreta de los datos solicitados por el usuario; pero la paginación personalizada implica un poco más de esfuerzo para implementar que la paginación predeterminada

Debido a la facilidad de implementación solo tiene que marcar una casilla y listo. la paginación predeterminada es una opción atractiva. Pero su enfoque sencillo para recuperar todos los registros, lo convierte en una opción poco plausible al paginar por cantidades grandes de datos o para sitios con muchos usuarios simultáneos. En esas circunstancias, debe recurrir a la paginación personalizada para proporcionar un sistema con capacidad de respuesta.

El desafío de la paginación personalizada es poder escribir una consulta que devuelva el conjunto preciso de registros necesarios para una página determinada de datos. Afortunadamente, Microsoft SQL Server 2005 proporciona una nueva palabra clave para clasificar resultados, lo que permite escribir una consulta que pueda recuperar eficazmente el subconjunto adecuado de registros. En este tutorial verá cómo usar esta nueva palabra clave de SQL Server 2005 para implementar la paginación personalizada en un control GridView. Aunque la interfaz de usuario para la paginación personalizada es idéntica a la de la paginación predeterminada, pasar de una página a la siguiente mediante paginación personalizada puede más rápido que la paginación predeterminada.

Nota:

La ganancia exacta de rendimiento mostrada por la paginación personalizada depende del número total de registros que se paginan y de la carga que se coloca en el servidor de bases de datos. Al final de este tutorial verá algunas métricas aproximadas que muestran las ventajas en el rendimiento obtenidos a través de la paginación personalizada.

Paso 1: Descripción del proceso de paginación personalizado

Al paginar por datos, los registros precisos mostrados en una página dependen de la página de datos que se solicita y del número de registros mostrados por página. Por ejemplo, imagine que quiere paginar por los 81 productos y mostrar 10 productos por página. Al ver la primera página, quiere los productos de 1 a 10; al ver la segunda página, los productos de 11 a 20, etc.

Hay tres variables que dictan qué registros deben recuperarse y cómo se debe representar la interfaz de paginación:

  • Índice de la fila inicial el índice de la primera fila de la página de datos para mostrar; este índice puede calcularse multiplicando el índice de la página por los registros para mostrar por página y sumando uno. Por ejemplo, al paginar los registros de 10 en 10, para la primera página (cuyo índice de página es 0), el índice de la fila inicial es 0 * 10 + 1, o 1; para la segunda página (cuyo índice de página es 1), el índice de la fila inicial es 1 * 10 + 1, o 11.
  • Número máximo de filas el número máximo de registros que se van a mostrar por página. Esta variable se conoce como filas máximas, ya que para la última página puede haber menos registros devueltos que el tamaño de página. Por ejemplo, al paginar por los 81 productos, 10 registros por página, la novena y última página tendrá un solo registro. Pero ninguna página mostrará más registros que el valor Máximo de filas.
  • Recuento total de registros el número total de registros que se paginan. Aunque esta variable no es necesaria para determinar qué registros se van a recuperar para una página determinada, determina la interfaz de paginación. Por ejemplo, si se paginan 81 productos, la interfaz de paginación sabe que debe mostrar nueve números de página en la interfaz de usuario de paginación.

Con la paginación predeterminada, el índice de la fila inicial se calcula como el producto del índice de página y el tamaño de página más uno, mientras que el tamaño máximo de filas es simplemente el tamaño de página. Como la paginación predeterminada recupera todos los registros de la base de datos al representar cualquier página de datos, se conoce el índice de cada fila, por lo que pasar a la fila de Índice de la fila inicial es una tarea trivial. Además, el recuento total de registros está fácilmente disponible, ya que es simplemente el número de registros de DataTable (o del objeto que se use para guardar los resultados de la base de datos).

Dadas las variables Índice de la fila inicial y Número máximo de filas, una implementación de paginación personalizada solo debe devolver el subconjunto preciso de registros que comienzan en el índice de la fila inicial hasta el número máximo de filas de registros posteriores. La paginación personalizada proporciona dos desafíos:

  • Debe poder asociar de forma eficaz un índice de fila a cada fila de todos los datos que se paginan para poder empezar a devolver registros en el índice de fila inicial especificado
  • Es necesario proporcionar el número total de registros que se paginan

En los dos pasos siguientes se examinará el script SQL necesario para responder a estos dos desafíos. Además del script SQL, también es necesario implementar métodos en DAL y BLL.

Paso 2: Devolución del número total de registros paginados

Antes de examinar cómo recuperar el subconjunto preciso de registros de la página que se muestra, primero verá cómo devolver el número total de registros que se paginan. Esta información es necesaria para configurar correctamente la interfaz de usuario de paginación. El número total de registros devueltos por una consulta SQL concreta se puede obtenerse mediante la función de agregación COUNT. Por ejemplo, para determinar el número total de registros de la tabla Products, se puede utilizar la siguiente consulta:

SELECT COUNT(*)
FROM Products

Ahora se agregará un método a la DAL que devuelve esta información. En concreto, se crearás un método de DAL llamado TotalNumberOfProducts() que ejecute la instrucción SELECT mostrada anteriormente.

Para empezar, abra el archivo Northwind.xsd de DataSet con tipo en la carpeta App_Code/DAL. A continuación, haga clic con el botón derecho en ProductsTableAdapter del Diseñador y seleccione Agregar consulta. Como ha visto en los tutoriales anteriores, esto nos permitirá agregar un nuevo método a la DAL que, cuando se invoca, ejecutará una instrucción SQL determinada o un procedimiento almacenado. Al igual que con los métodos TableAdapter de los tutoriales anteriores, para este opte por usar una instrucción SQL ad hoc.

Use an Ad-Hoc SQL Statement

Figura 1: Uso de una instrucción SQL ad hoc

En la siguiente pantalla, puede especificar qué tipo de consulta se va a crear. Como esta consulta devolverá un único valor escalar, el número total de registros de la tabla Products, elija la opción SELECT, que devuelve un único valor.

Configure the Query to Use a SELECT Statement that Returns a Single Value

Figura 2: Configuración de la consulta para usar una instrucción SELECT que devuelve un valor único

Después de indicar el tipo de consulta que se va a usar, debe especificar la consulta.

Use the SELECT COUNT(*) FROM Products Query

Figura 3: Uso de la consulta SELECT COUNT(*) FROM Products

Por último, especifique el nombre del método. Como se ha mencionado antes, se usará TotalNumberOfProducts.

Name the DAL Method TotalNumberOfProducts

Figura 4: Asignación del nombre TotalNumberOfProducts a método de la DAL

Tras hacer clic en Finalizar, el asistente añadirá el método TotalNumberOfProducts a la DAL. Los métodos de devolución escalares de la DAL devuelven tipos que aceptan valores NULL, en caso de que el resultado de la consulta SQL sea NULL. Pero la consulta COUNT siempre devolverá un valor distinto de NULL; independientemente de ello, el método DAL devuelve un entero que admite un valor NULL.

Además del método la DAL, también necesita un método en la BLL. Abra el archivo de clase ProductsBLL y agregue un método TotalNumberOfProducts que simplemente llame al método TotalNumberOfProducts de la DAL:

public int TotalNumberOfProducts()
{
    return Adapter.TotalNumberOfProducts().GetValueOrDefault();
}

El método TotalNumberOfProducts de la DAL devuelve un entero que admite un valor NULL; pero se ha creado el método TotalNumberOfProducts de la clase ProductsBLL para que devuelva un entero estándar. Por tanto, necesita que el método TotalNumberOfProducts de la clase ProductsBLL devuelva la parte de valor del entero que admite un valor NULL devuelto por el método TotalNumberOfProducts de la DAL. La llamada a GetValueOrDefault() devuelve el valor del entero que admite un valor NULL, si existe; pero, si el entero que admite un valor NULL es null, devuelve el valor entero predeterminado, 0.

Paso 3: Devolución del subconjunto preciso de registros

La siguiente tarea consiste en crear métodos en la DAL y BLL que acepten las variables Índice de la fila inicial y Número máximo de filas que se trataron anteriormente y devuelven los registros adecuados. Antes de hacerlo, primero verá el script SQL necesario. El desafío que se presenta es que debe poder asignar eficazmente un índice a cada fila de los resultados que se paginan por completo para poder devolver solo esos registros a partir del índice de fila inicial (y hasta el número máximo de registros).

Esto no es un desafío si ya hay una columna en la tabla de base de datos que actúa como índice de fila. A primera vista podría pensar que el campo ProductID de la tabla Products sería suficiente, ya que el primer producto tiene ProductID de 1, el segundo un 2, y así sucesivamente. Pero la eliminación de un producto deja un hueco en la secuencia, lo que anula este enfoque.

Hay dos técnicas generales que se usan para asociar eficazmente un índice de fila con los datos a la página, lo que permite recuperar el subconjunto preciso de registros:

  • Usar la palabra clave ROW_NUMBER() de SQL Server 2005, la palabra clave ROW_NUMBER(), novedad de SQL Server 2005, asocia una clasificación a cada registro devuelto en función de algún orden. Esta clasificación se puede usar como índice de fila para cada fila.

  • Usar una variable de tabla y SET ROWCOUNT; la instrucción SET ROWCOUNT de SQL Server se puede usar para especificar cuántos registros totales debe procesar una consulta antes de finalizar; las variables de tabla son variables T-SQL locales que pueden contener datos tabulares, similares a las tablas temporales. Este enfoque funciona igualmente bien con Microsoft SQL Server 2005 y SQL Server 2000 (mientras que el enfoque ROW_NUMBER() solo funciona con SQL Server 2005).

    La idea aquí es crear una variable de tabla que tenga una columna IDENTITY y columnas para las claves principales de la tabla cuyos datos se paginan. A continuación, el contenido de la tabla cuyos datos se están paginando se vuelca en la variable de tabla, y se asocia un índice de fila secuencial (mediante la columna IDENTITY) para cada registro de la tabla. Una vez que se rellena la variable de tabla, se puede ejecutar una instrucción SELECT sobre la variable de tabla, unida a la tabla subyacente, para extraer los registros concretos. La instrucción SET ROWCOUNT se utiliza para limitar de forma inteligente el número de registros que deben volcarse en la variable de tabla.

    La eficacia de este enfoque se basa en el número de página que se solicita, ya que al valor SET ROWCOUNT se le asigna el valor del Índice de la fila inicial más las filas máximas. Al paginar por páginas con números bajos, como las primeras páginas de datos, este enfoque es muy eficaz. Pero muestra un rendimiento similar al de la predeterminada paginación al recuperar una página cerca del final.

En este tutorial se implementa la paginación personalizada mediante la palabra clave ROW_NUMBER(). Para más información sobre el uso de la variable de tabla y la técnica SET ROWCOUNT, vea Paginación eficaz por grandes conjuntos de datos.

La palabra clave ROW_NUMBER() asocia una clasificación a cada registro devuelto sobre una ordenación determinada utilizando la siguiente sintaxis:

SELECT columnList,
       ROW_NUMBER() OVER(orderByClause)
FROM TableName

ROW_NUMBER() devuelve un valor numérico que especifica la clasificación de cada registro con respecto a la ordenación indicada. Por ejemplo, para ver la clasificación de cada producto, ordenado del más caro al menos caro, se podría utilizar la siguiente consulta:

SELECT ProductName, UnitPrice,
       ROW_NUMBER() OVER(ORDER BY UnitPrice DESC) AS PriceRank
FROM Products

En la figura 5 se muestran los resultados de esta consulta cuando se ejecuta desde la ventana de consulta en Visual Studio. Tenga en cuenta que los productos se ordenan por precio, junto con una clasificación de precios para cada fila.

The Price Rank is Included for Each Returned Record

Figura 5: La clasificación de precios se incluye para cada registro devuelto

Nota:

ROW_NUMBER() es solo una de las muchas nuevas funciones de clasificación disponibles en SQL Server 2005. Para obtener una explicación más exhaustiva de ROW_NUMBER(), junto con las otras funciones de clasificación, lea Devolución de resultados clasificados con Microsoft SQL Server 2005.

Al clasificar por orden de prioridad los resultados por la columna ORDER BY especificada en la cláusula OVER (UnitPrice, en el ejemplo anterior), SQL Server debe ordenar los resultados. Se trata de una operación rápida si hay un índice agrupado sobre las columnas por las que se ordenan los resultados, o si hay un índice de cobertura, pero puede ser más costoso. Para ayudar a mejorar el rendimiento de las consultas suficientemente grandes, considere la posibilidad de agregar un índice no agrupado para la columna por la que se ordenan los resultados. Vea Funciones de clasificación y rendimiento en SQL Server 2005 para obtener información más detallada sobre las consideraciones de rendimiento.

La información de clasificación devuelta por ROW_NUMBER() no puede utilizarse directamente en la cláusula WHERE. Pero se puede utilizar una tabla derivada para devolver el resultado ROW_NUMBER(), que luego puede aparecer en la cláusula WHERE. Por ejemplo, la siguiente consulta utiliza una tabla derivada para devolver las columnas ProductName y UnitPrice, junto con el resultado ROW_NUMBER(), y después utiliza una cláusula WHERE para devolver únicamente aquellos productos cuyo rango de precios se encuentre entre 11 y 20:

SELECT PriceRank, ProductName, UnitPrice
FROM
   (SELECT ProductName, UnitPrice,
       ROW_NUMBER() OVER(ORDER BY UnitPrice DESC) AS PriceRank
    FROM Products
   ) AS ProductsWithRowNumber
WHERE PriceRank BETWEEN 11 AND 20

Al ampliar este concepto un poco más, se puede usar este enfoque para recuperar una página específica de datos según los valores deseados de Índice de la fila inicial y Número máximo de filas:

SELECT PriceRank, ProductName, UnitPrice
FROM
   (SELECT ProductName, UnitPrice,
       ROW_NUMBER() OVER(ORDER BY UnitPrice DESC) AS PriceRank
    FROM Products
   ) AS ProductsWithRowNumber
WHERE PriceRank > <i>StartRowIndex</i> AND
    PriceRank <= (<i>StartRowIndex</i> + <i>MaximumRows</i>)

Nota:

Como verá más adelante en este tutorial, el valor StartRowIndex suministrado por el ObjectDataSource está indexado empezando por cero, mientras que el valor ROW_NUMBER() devuelto por SQL Server 2005 está indexado empezando por 1. Por tanto, la cláusula WHERE devuelve aquellos registros en los que PriceRank es estrictamente mayor que StartRowIndex y menor o igual que StartRowIndex + MaximumRows.

Ahora que ha visto cómo se puede utilizar ROW_NUMBER() para recuperar una página concreta de datos teniendo en cuenta los valores Índice de la fila inicial y Número máximo de filas, hay que implementar esta lógica como métodos en DAL y BLL.

Al crear esta consulta, debe decidir la ordenación por la que se clasificarán los resultados; los productos se ordenarán por su nombre en orden alfabético. Esto significa que con la implementación de paginación personalizada en este tutorial no podrá crear un informe paginado personalizado que también se pueda ordenar. Pero en el siguiente tutorial verá cómo se puede proporcionar esa funcionalidad.

En la sección anterior ha creado el método de la DAL como una instrucción SQL ad hoc. Desafortunadamente, al analizador T-SQL de Visual Studio utilizado por el asistente para TableAdapter no le gusta la sintaxis OVER utilizada por la función ROW_NUMBER(). Por tanto, debe crear este método de la DAL como un procedimiento almacenado. Seleccione el Explorador de servidores en el menú Ver (o presiones Ctrl+Alt+S) y expanda el nodo NORTHWND.MDF. Para agregar un nuevo procedimiento almacenado, haga clic con el botón derecho en el nodo Procedimientos almacenados y elija Agregar un nuevo procedimiento almacenado (vea la figura 6).

Add a New Stored Procedure for Paging Through the Products

Figura 6: Adición de un nuevo procedimiento almacenado para paginar por los productos

Este procedimiento almacenado debe aceptar dos parámetros de entrada enteros, @startRowIndex y @maximumRows, y utilizar la función ROW_NUMBER() ordenada por el campo ProductName, devolviendo solo aquellas filas mayores que el valor @startRowIndex especificado y menores o iguales que @startRowIndex + @maximumRow. Escriba el siguiente script en el nuevo procedimiento almacenado y, después, haga clic en el icono Guardar para agregarlo a la base de datos.

CREATE PROCEDURE dbo.GetProductsPaged
(
    @startRowIndex int,
    @maximumRows int
)
AS
    SELECT     ProductID, ProductName, SupplierID, CategoryID, QuantityPerUnit,
               UnitPrice, UnitsInStock, UnitsOnOrder, ReorderLevel, Discontinued,
               CategoryName, SupplierName
FROM
   (
       SELECT ProductID, ProductName, SupplierID, CategoryID, QuantityPerUnit,
              UnitPrice, UnitsInStock, UnitsOnOrder, ReorderLevel, Discontinued,
              (SELECT CategoryName
               FROM Categories
               WHERE Categories.CategoryID = Products.CategoryID) AS CategoryName,
              (SELECT CompanyName
               FROM Suppliers
               WHERE Suppliers.SupplierID = Products.SupplierID) AS SupplierName,
              ROW_NUMBER() OVER (ORDER BY ProductName) AS RowRank
        FROM Products
    ) AS ProductsWithRowNumbers
WHERE RowRank > @startRowIndex AND RowRank <= (@startRowIndex + @maximumRows)

Después de crear el procedimiento almacenado, dedique un momento a probarlo. Haga clic con el botón derecho en el nombre del procedimiento almacenado GetProductsPaged en el Explorador de servidores y elija la opción Ejecutar. Visual Studio le pedirá los parámetros de entrada, @startRowIndex y @maximumRow (véase la figura 7). Pruebe valores diferentes y examine los resultados.

Enter a Value for the <span class=Parámetros @startRowIndex y @maximumRows" />

Figura 7: Un valor para los parámetros @startRowIndex y @maximumRows

Después de elegir estos valores de parámetros de entrada, la ventana Salida mostrará los resultados. En la figura 8 se muestran los resultados al pasar 10 para los parámetros @startRowIndex y @maximumRows.

The Records That Would Appear in the Second Page of Data are Returned

Figura 8: Se devuelven los registros que aparecerían en la segunda página de datos (Haga clic para ver la imagen a tamaño completo)

Con este procedimiento almacenado creado, ya puede crear el método ProductsTableAdapter. Abra el conjunto de datos con tipo Northwind.xsd, haga clic con el botón derecho del ratón en ProductsTableAdapter y elija la opción Agregar consulta. En lugar de crear la consulta mediante una instrucción SQL ad hoc, créela mediante un procedimiento almacenado existente.

Create the DAL Method Using an Existing Stored Procedure

Figura 9: Creación del método de la DAL mediante un procedimiento almacenado existente

A continuación, se le pedirá que seleccione el procedimiento almacenado que se va a invocar. Elija el procedimiento almacenado GetProductsPaged en la lista desplegable.

Choose the GetProductsPaged Stored Procedure from the Drop-Down List

Figura 10: Elección del procedimiento almacenado GetProductsPaged en la lista desplegable

A continuación, la siguiente pantalla le pregunta qué tipo de datos devuelve el procedimiento almacenado: datos tabulares, un valor único o ningún valor. Como el procedimiento almacenado GetProductsPaged puede devolver varios registros, indique que devuelve datos tabulares.

Indicate that the Stored Procedure Returns Tabular Data

Figura 11: Indicación de que el procedimiento almacenado devuelve datos tabulares

Por último, indique los nombres de los métodos que quiera crear. Al igual que con los tutoriales anteriores, continúe y cree métodos mediante Fill a DataTable y Return a DataTable. Asigne el nombre FillPaged al primer método y GetProductsPaged al segundo.

Name the Methods FillPaged and GetProductsPaged

Figura 12: Asignación de un nombre a los métodos FillPaged y GetProductsPaged

Además de crear un método de la DAL para devolver una página determinada de productos, también es necesario proporcionar esa funcionalidad en la BLL. Al igual que el método DAL, el método GetProductsPaged para la BLL debe aceptar dos entradas enteras para especificar el índice de fila inicial y las filas máximas, y debe devolver solo los registros que se encuentran dentro del intervalo especificado. Cree un método de la BLL de este tipo en la clase ProductsBLL que simplemente llame al método GetProductsPaged de la DAL, de la siguiente manera:

[System.ComponentModel.DataObjectMethodAttribute(
    System.ComponentModel.DataObjectMethodType.Select, false)]
public Northwind.ProductsDataTable GetProductsPaged(int startRowIndex, int maximumRows)
{
    return Adapter.GetProductsPaged(startRowIndex, maximumRows);
}

Puede usar cualquier nombre para los parámetros de entrada del método de la BLL, pero, como verá en breve, al elegir startRowIndex y maximumRows se ahorra un poco de trabajo extra a la hora de configurar una instancia de ObjectDataSource para usar este método.

Paso 4: Configuración de ObjectDataSource para usar la paginación personalizada

Con los métodos de la BLL y DAL para acceder a un subconjunto determinado de registros completados, ya puede crear un control GridView que pagine por sus registros subyacentes mediante la paginación personalizada. Para empezar, abra la página EfficientPaging.aspx en la carpeta PagingAndSorting, añada un control GridView a la página y configúrelo para que utilice un nuevo control ObjectDataSource. En los tutoriales anteriores, a menudo se configuraba ObjectDataSource para utilizar el método GetProducts de la clase ProductsBLL. Pero esta vez quiere utilizar el método GetProductsPaged en su lugar, ya que el método GetProducts devuelve todos los productos de la base de datos, mientras que GetProductsPaged solo devuelve un subconjunto concreto de registros.

Configure the ObjectDataSource to Use the ProductsBLL Class s GetProductsPaged Method

Figura 13: Configuración de ObjectDataSource para usar el método GetProductsPaged de la clase ProductsBLL

Como se va crear un control GridView de solo lectura, tómese un momento para establecer la lista desplegable de métodos en las pestañas INSERT, UPDATE y DELETE en (None).

A continuación, el asistente para ObjectDataSource solicita los orígenes de los valores de los parámetros de entrada startRowIndex y maximumRows del método GetProductsPaged. En realidad, estos parámetros de entrada los establece el control GridView automáticamente, así que simplemente deje el origen establecido en Ninguno y haga clic en Finalizar.

Leave the Input Parameter Sources as None

Figura 14: Orígenes de parámetros de entrada establecidos en Ninguno

Después de completar el asistente para ObjectDataSource, GridView contendrá un control BoundField o CheckBoxField para cada uno de los campos de datos del producto. No dude en adaptar la apariencia de GridView como prefiera. Aquí se ha optado por mostrar solo las instancias ProductName, CategoryName, SupplierName, QuantityPerUnit y UnitPrice de BoundField. Además, configure GridView para admitir la paginación; para ello, active la casilla Habilitar paginación en su etiqueta inteligente. Tras estos cambios, el marcado declarativo de GridView y ObjectDataSource debería tener un aspecto similar al siguiente:

<asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="False"
    DataKeyNames="ProductID" DataSourceID="ObjectDataSource1" AllowPaging="True">
    <Columns>
        <asp:BoundField DataField="ProductName" HeaderText="Product"
            SortExpression="ProductName" />
        <asp:BoundField DataField="CategoryName" HeaderText="Category"
            ReadOnly="True" SortExpression="CategoryName" />
        <asp:BoundField DataField="SupplierName" HeaderText="Supplier"
            SortExpression="SupplierName" />
        <asp:BoundField DataField="QuantityPerUnit" HeaderText="Qty/Unit"
            SortExpression="QuantityPerUnit" />
        <asp:BoundField DataField="UnitPrice" DataFormatString="{0:c}"
            HeaderText="Price" HtmlEncode="False" SortExpression="UnitPrice" />
    </Columns>
</asp:GridView>
<asp:ObjectDataSource ID="ObjectDataSource1" runat="server"
    OldValuesParameterFormatString="original_{0}" SelectMethod="GetProductsPaged"
    TypeName="ProductsBLL">
    <SelectParameters>
        <asp:Parameter Name="startRowIndex" Type="Int32" />
        <asp:Parameter Name="maximumRows" Type="Int32" />
    </SelectParameters>
</asp:ObjectDataSource>

Pero si visita la página en un explorador, no encontrará el control GridView.

The GridView is Not Displayed

Figura 15: GridView no se muestra

Falta GridView porque ObjectDataSource utiliza actualmente 0 como valor para los dos parámetros de entrada GetProductsPagedstartRowIndex y maximumRows. Por tanto, la consulta SQL resultante no devuelve ningún registro y, por tanto, no se muestra GridView.

Para solucionar esto, es necesario configurar ObjectDataSource para usar la paginación personalizada. Esto se puede realizar en los pasos siguientes:

  1. Establecer la propiedad EnablePaging de ObjectDataSource entrue; esto indica a ObjectDataSource que debe pasar a SelectMethod dos parámetros adicionales: uno para especificar el Índice de la fila inicial (StartRowIndexParameterName) y otro para especificar el Número máximo de filas (MaximumRowsParameterName).
  2. Establecer las propiedades StartRowIndexParameterName y MaximumRowsParameterName de ObjectDataSource en consecuencia; las propiedades StartRowIndexParameterName y MaximumRowsParameterName indican los nombres de los parámetros de entrada pasados a SelectMethod par la paginación personalizada. De forma predeterminada, estos nombres de parámetros son startIndexRow y maximumRows, por lo que, al crear el método GetProductsPaged en la BLL, se han usado estos valores para los parámetros de entrada. Si decidiera usar otros nombres de parámetro para el método GetProductsPaged de BLL, como startIndex y maxRows, por ejemplo, tendría que configurar las propiedades StartRowIndexParameterName y MaximumRowsParameterName de ObjectDataSource en consecuencia (como startIndex para StartRowIndexParameterName y maxRows para MaximumRowsParameterName).
  3. Establecer la propiedad SelectCountMethod de ObjectDataSource en el nombre del método que devuelve el número total de registros paginados (TotalNumberOfProducts); recuerde que el método TotalNumberOfProducts de la clase ProductsBLL devuelve el número total de registros paginados mediante un método DAL que ejecuta una consulta SELECT COUNT(*) FROM Products. ObjectDataSource necesita esta información para representar correctamente la interfaz de paginación.
  4. Eliminar los elementos startRowIndex y maximumRows<asp:Parameter> del marcado declarativo de ObjectDataSource; al configurar ObjectDataSource con el asistente, Visual Studio ha agregado automáticamente dos elementos <asp:Parameter> para los parámetros de entrada del método GetProductsPaged. Al establecer EnablePaging en true, estos parámetros se pasarán automáticamente; si también aparecen en la sintaxis declarativa, ObjectDataSource intentará pasar cuatro parámetros al método GetProductsPaged y dos parámetros al método TotalNumberOfProducts. Si se olvida de eliminar estos elementos <asp:Parameter>, al visitar la página en un navegador obtendrá un mensaje de error del tipo ObjectDataSource "ObjectDataSource1" no pudo encontrar un método no genérico "TotalNumberOfProducts" que tenga los parámetros: startRowIndex, maximumRows.

Después de realizar estos cambios, la sintaxis declarativa de ObjectDataSource debe ser similar a la siguiente:

<asp:ObjectDataSource ID="ObjectDataSource1" runat="server"
    OldValuesParameterFormatString="original_{0}" TypeName="ProductsBLL"
    SelectMethod="GetProductsPaged" EnablePaging="True"
    SelectCountMethod="TotalNumberOfProducts">
</asp:ObjectDataSource>

Observe que se han establecido las propiedades EnablePaging y SelectCountMethod, y se han eliminado los elementos <asp:Parameter>. En la figura 16 se muestra una captura de pantalla de la ventana Propiedades después de realizar estos cambios.

To Use Custom Paging, Configure the ObjectDataSource Control

Figura 16: Para usar la paginación personalizada, se configura el control ObjectDataSource

Después de realizar estos cambios, visite esta página mediante un explorador. Debería ver 10 productos en la lista, ordenados alfabéticamente. Dedique un momento a recorrer los datos de una página a la vez. Aunque no hay ninguna diferencia visual desde la perspectiva del usuario final entre la paginación predeterminada y la paginación personalizada, la paginación personalizada es más eficaz en las páginas con grandes cantidades de datos, ya que solo recupera los registros que deben mostrarse para una página determinada.

The Data, Ordered by the Product s Name, is Paged Using Custom Paging

Figura 17: Los datos, ordenados por el nombre del producto, se ordenan mediante paginación personalizada (Haga clic para ver la imagen a tamaño completo)

Nota:

Con la paginación personalizada, el valor del recuento de páginas devuelto por SelectCountMethod de ObjectDataSource se almacena en el estado de visualización del GridView. Otras variables de GridView, la colección PageIndex, EditIndex, SelectedIndex, DataKeys, etc. se almacenan en el estado de control, que se mantiene independientemente del valor de la propiedad EnableViewState de GridView. Como el valor PageCount se conserva entre postbacks utilizando el estado de visualización, cuando utilice una interfaz de paginación que incluya un enlace que le lleve a la última página, es imprescindible que el estado de visualización del control GridView esté activado. (Si la interfaz de paginación no incluye un vínculo directo a la última página, puede deshabilitar el estado de visualización).

Al hacer clic en el enlace de la última página se produce un postback y se ordena al GridView que actualice su propiedad PageIndex. Si se hace clic en el enlace de la última página, el control GridView asigna a su propiedad PageIndex un valor una unidad menos que el de su propiedad PageCount. Con el estado de visualización desactivado, el valor PageCount se pierde entre postbacks y a PageIndex se le asigna en su lugar el valor entero máximo. A continuación, el control GridView intenta determinar el índice de la fila inicial multiplicando las propiedades PageSize y PageCount. Esto da como resultadoOverflowException ya que el producto excede el tamaño entero máximo permitido.

Implementación de paginación y ordenación personalizadas

La implementación de paginación personalizada actual requiere que el orden de paginación de los datos se especifique estáticamente al crear el procedimiento almacenado GetProductsPaged. Pero es posible que haya observado que la etiqueta inteligente de GridView contiene una casilla Habilitar ordenación además de la opción Habilitar paginación. Lamentablemente, si se agrega compatibilidad con la ordenación al control GridView con la implementación de paginación personalizada actual, solo se ordenarán los registros de la página de datos visualizada en ese momento. Por ejemplo, si configura GridView para admitir también la paginación y, después, al ver la primera página de datos, ordena por nombre de producto en orden descendente, revertirá el orden de los productos en la página 1. Como se muestra en la figura 18, Carnarvon Tigers es el primer producto cuando se ordena en orden alfabético inverso, lo que ignora los otros 71 productos que vienen después de Carnarvon Tigers, alfabéticamente; en la ordenación solo se tienen en cuenta los registros de la primera página.

Only the Data Shown on the Current Page is Sorted

Figura 18: Solo se ordenan los datos mostrados en la página actual (Haga clic para ver la imagen a tamaño completo)

La ordenación solo se aplica a la página actual de datos porque se produce después de que los datos se hayan recuperado del método GetProductsPaged de la BLL, y este método solo devuelve los registros de la página específica. Para implementar la ordenación correctamente, es necesario pasar la expresión de ordenación al método GetProductsPaged para que los datos puedan ordenarse adecuadamente antes de devolver la página específica de datos. Verá cómo hacerlo en el siguiente tutorial.

Implementación de paginación y eliminación personalizadas

Si habilita la funcionalidad de eliminación en un control GridView cuyos datos se paginan utilizando técnicas de paginación personalizadas, comprobará que al borrar el último registro de la última página, GridView desaparece en lugar de disminuir apropiadamente el valor PageIndex. Para reproducir este error, habilite la eliminación para el tutorial que acaba de crear. Vaya a la última página (página 9), donde debería ver un solo producto, ya que se pagina por 81 productos, 10 productos a la vez. Elimine este producto.

Al eliminar el último producto, GridView debería pasar automáticamente a la octava página, y esa funcionalidad se exhibe con la paginación predeterminada. Pero con la paginación personalizada, después de eliminar ese último producto en la última página, GridView simplemente desaparece de la pantalla por completo. La razón precisa de por qué ocurre esto está un poco más allá del ámbito de este tutorial; vea Eliminación del último registro de la última página de un control GridView con la paginación personalizada para obtener los detalles de bajo nivel sobre el origen de este problema. En resumen, se debe a la siguiente secuencia de pasos que realiza GridView cuando se hace clic en el botón Eliminar:

  1. Eliminar el registro
  2. Obtener los registros apropiados para mostrar para los valor PageIndex y PageSize especificados
  3. Comprobar que PageIndex no supera el número de páginas de datos del origen de datos; si es así, se disminuye automáticamente la propiedad PageIndex de GridView
  4. Enlazar la página adecuada de datos a GridView mediante los registros obtenidos en el paso 2

El problema proviene del hecho de que en el paso 2 la instancia de PageIndex utilizada al obtener los registros para mostrar sigue siendo el valor PageIndex de la última página cuyo único registro se acaba de borrar. Por tanto, en el paso 2, no se devuelven registros puesto que esa última página de datos ya no contiene registros. Después, en el paso 3, GridView se da cuenta de que su propiedad PageIndex es mayor que el número total de páginas del origen de datos (ya que se ha eliminado el último registro de la última página) y, por tanto, disminuye su propiedad PageIndex. En el paso 4, GridView intenta enlazarse a los datos recuperados en el paso 2; pero en el paso 2 no se devolvieron registros, por lo que se creó un elemento GridView vacío. Con la paginación predeterminada, este problema no surge porque en el paso 2 todos los registros se recuperan del origen de datos.

Para corregir esto, hay dos opciones. La primera es crear un controlador de eventos para el controlador de eventos RowDeleted de GridView que determine cuántos registros se mostraron en la página que se acaba de eliminar. Si solo había un registro, entonces el registro que se acaba de eliminar debe haber sido el último y hay que disminuir PageIndex de GridView. Por supuesto, solo quiere actualizar PageIndex si la operación de eliminación ha tenido éxito realmente, lo que puede determinar si se asegura de que la propiedad e.Exception es null.

Este enfoque funciona porque actualiza el PageIndex después del paso 1 pero antes del paso 2. Por tanto, en el paso 2 se devuelve el conjunto adecuado de registros. Para ello, use código como el siguiente:

protected void GridView1_RowDeleted(object sender, GridViewDeletedEventArgs e)
{
    // If we just deleted the last row in the GridView, decrement the PageIndex
    if (e.Exception == null && GridView1.Rows.Count == 1)
        // we just deleted the last row
        GridView1.PageIndex = Math.Max(0, GridView1.PageIndex - 1);
}

Una solución alternativa es crear un controlador de eventos para el evento RowDeleted de ObjectDataSource y establecer la propiedad AffectedRows en el valor 1. Después de eliminar el registro en el paso 1 (pero antes de volver a recuperar los datos en el paso 2), GridView actualiza su propiedad PageIndex si una o más filas se han visto afectadas por la operación. Pero la propiedad AffectedRows no es establecida por ObjectDataSource y, por tanto, se omite este paso. Una forma de hacer que se ejecute este paso es establecer manualmente la propiedad AffectedRows si la operación de eliminación se completa con éxito. Esto se puede lograr mediante código como el siguiente:

protected void ObjectDataSource1_Deleted(
    object sender, ObjectDataSourceStatusEventArgs e)
{
    // If we get back a Boolean value from the DeleteProduct method and it's true,
    // then we successfully deleted the product. Set AffectedRows to 1
    if (e.ReturnValue is bool && ((bool)e.ReturnValue) == true)
        e.AffectedRows = 1;
}

El código de ambos controladores de eventos se encuentra en la clase de código subyacente del ejemplo EfficientPaging.aspx.

Comparación del rendimiento de la paginación predeterminada y personalizada

Como la paginación personalizada solo recupera los registros necesarios, mientras que la paginación predeterminada devuelve todos los registros de cada página que se visualiza, está claro que la paginación personalizada es más eficiente que la predeterminada. ¿Pero cuánta eficacia más ofrece la paginación personalizada? ¿Qué tipo de mejoras de rendimiento se pueden ver al pasar de la paginación predeterminada a la paginación personalizada?

Desafortunadamente, no hay una respuesta única. La ganancia de rendimiento depende de varios factores, los dos más destacados son el número de registros que se paginan y la carga colocada en el servidor de base de datos y los canales de comunicación entre el servidor web y el servidor de base de datos. Para tablas pequeñas con solo unas docenas de registros, la diferencia de rendimiento puede ser insignificante. En el caso de las tablas grandes; pero con miles o cientos de miles de filas, la diferencia de rendimiento es considerable.

Mi artículo, "Paginación personalizada en ASP.NET 2.0 con SQL Server 2005", contiene algunas pruebas de rendimiento que he ejecutado para mostrar las diferencias de rendimiento entre estas dos técnicas de paginación al paginar en una tabla de base de datos con 50 000 registros. En estas pruebas examiné tanto el tiempo de ejecución de la consulta a nivel de SQL Server (con SQL Profiler) como a nivel de la página ASP.NET mediante funciones de seguimiento de ASP.NET. Tenga en cuenta que estas pruebas se ejecutaron en mi entorno de desarrollo con un solo usuario activo y, por tanto, no son científicas y no imitan los modelos de carga de sitios web típicos. Independientemente de los resultados, muestran las diferencias relativas en el tiempo de ejecución para la paginación predeterminada y personalizada cuando se trabaja con cantidades suficientemente grandes de datos.

Promedio de duración (segundos) Reads
Paginación predeterminada de SQL Profiler 1,411 383
Paginación personalizada de SQL Profiler 0,002 29
Seguimiento ASP.NET de paginación predeterminada 2,379 N/D
Seguimiento ASP.NET de paginación personalizada 0.029 N/D

Como puede ver, recuperar una página determinada de datos requería un promedio de 354 lecturas menos y se completaba en una fracción del tiempo. En la página ASP.NET, la página personalizada se pudo representar cerca del 1/100 del tiempo que tardó la paginación predeterminada.

Resumen

La paginación predeterminada es muy fácil de implementar, basta con activar la casilla Habilitar paginación en la etiqueta inteligente del control web de datos, pero esa simplicidad se produce a costa del rendimiento. Con la paginación predeterminada, cuando un usuario solicita cualquier página de datos se devuelven todos los registros, aunque solo se muestre una pequeña fracción de ellos. Para combatir esta sobrecarga de rendimiento, ObjectDataSource ofrece una opción alternativa de paginación personalizada.

Aunque la paginación personalizada mejora los problemas de rendimiento de la paginación predeterminada recuperando solo los registros que deben mostrarse, es más complicado implementar la paginación personalizada. En primer lugar, se debe escribir una consulta que acceda correctamente (y eficazmente) al subconjunto específico de registros solicitados. Esto puede lograrse de varias formas; la que se examina en este tutorial consiste en utilizar la nueva función ROW_NUMBER() de SQL Server 2005 para clasificar los resultados y, después, devolver solo aquellos cuya clasificación se encuentre dentro de un rango especificado. Además, es necesario agregar un medio para determinar el número total de registros que se paginan. Después de crear estos métodos para la DAL y BLL, también es necesario configurar ObjectDataSource para que pueda determinar cuántos registros totales se paginan y pueden pasar correctamente los valores Índice de la fila inicial y Número máximo de filas a la BLL.

Aunque la implementación de la paginación personalizada requiere una serie de pasos y no es tan simple como la paginación predeterminada, la paginación personalizada es una necesidad al paginar por cantidades grandes de datos. Como se ha mostrado en los resultados examinados, la paginación personalizada puede perder segundos del tiempo de representación de la página ASP.NET y aligerar la carga en el servidor de bases de datos por uno o varios órdenes de magnitud.

¡Feliz programación!

Acerca del autor

Scott Mitchell, autor de siete libros de ASP y ASP.NET, y fundador de 4GuysFromRolla.com, trabaja con tecnologías web de Microsoft desde 1998. Scott trabaja como consultor independiente, entrenador y escritor. Su último libro es Sams Teach Yourself ASP.NET 2.0 in 24 Hours. Puede ponerse en contacto con él a través de mitchell@4GuysFromRolla.com. o de su blog, que se puede encontrar en http://ScottOnWriting.NET.