Pasar página por grandes cantidades de datos de forma eficaz (C#)
por Scott Mitchell
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.
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.
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.
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
.
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 claveROW_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ónSET 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 enfoqueROW_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 columnaIDENTITY
) para cada registro de la tabla. Una vez que se rellena la variable de tabla, se puede ejecutar una instrucciónSELECT
sobre la variable de tabla, unida a la tabla subyacente, para extraer los registros concretos. La instrucciónSET 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.
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).
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.
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
.
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.
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.
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.
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.
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.
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.
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.
Figura 15: GridView no se muestra
Falta GridView porque ObjectDataSource utiliza actualmente 0 como valor para los dos parámetros de entrada GetProductsPaged
, startRowIndex
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:
- Establecer la propiedad
EnablePaging
de ObjectDataSource entrue
; esto indica a ObjectDataSource que debe pasar aSelectMethod
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
). - Establecer las propiedades
StartRowIndexParameterName
yMaximumRowsParameterName
de ObjectDataSource en consecuencia; las propiedadesStartRowIndexParameterName
yMaximumRowsParameterName
indican los nombres de los parámetros de entrada pasados aSelectMethod
par la paginación personalizada. De forma predeterminada, estos nombres de parámetros sonstartIndexRow
ymaximumRows
, por lo que, al crear el métodoGetProductsPaged
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étodoGetProductsPaged
de BLL, comostartIndex
ymaxRows
, por ejemplo, tendría que configurar las propiedadesStartRowIndexParameterName
yMaximumRowsParameterName
de ObjectDataSource en consecuencia (como startIndex paraStartRowIndexParameterName
y maxRows paraMaximumRowsParameterName
). - 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étodoTotalNumberOfProducts
de la claseProductsBLL
devuelve el número total de registros paginados mediante un método DAL que ejecuta una consultaSELECT COUNT(*) FROM Products
. ObjectDataSource necesita esta información para representar correctamente la interfaz de paginación. - Eliminar los elementos
startRowIndex
ymaximumRows
<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étodoGetProductsPaged
. Al establecerEnablePaging
entrue
, estos parámetros se pasarán automáticamente; si también aparecen en la sintaxis declarativa, ObjectDataSource intentará pasar cuatro parámetros al métodoGetProductsPaged
y dos parámetros al métodoTotalNumberOfProducts
. 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.
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.
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.
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:
- Eliminar el registro
- Obtener los registros apropiados para mostrar para los valor
PageIndex
yPageSize
especificados - 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 propiedadPageIndex
de GridView - 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/ASP.NET y fundador de 4GuysFromRolla.com, ha trabajado 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. Puedes ponerte en contacto con él en mitchell@4GuysFromRolla.com. o a través de su blog, http://ScottOnWriting.NET.