Paginação de grandes quantidades de dados com eficiência (C#)
por Scott Mitchell
A opção de paginação padrão de um controle de apresentação de dados não é adequada ao trabalhar com grandes quantidades de dados, pois seu controle de fonte de dados subjacente recupera todos os registros, mesmo que apenas um subconjunto de dados seja exibido. Nessas circunstâncias, devemos recorrer à paginação personalizada.
Introdução
Como discutimos no tutorial anterior, a paginação pode ser implementada de duas maneiras:
- A paginação padrão pode ser implementada simplesmente marcando a opção Habilitar paginação na marca inteligente do controle Web de dados; no entanto, sempre que exibir uma página de dados, o ObjectDataSource recupera todos os registros, mesmo que apenas um subconjunto deles seja exibido na página
- A paginação personalizada melhora o desempenho da paginação padrão, recuperando apenas os registros do banco de dados que precisam ser exibidos para a página específica de dados solicitada pelo usuário; no entanto, a paginação personalizada envolve um pouco mais de esforço para implementar do que a paginação padrão
Devido à facilidade de implementação, basta marcar uma caixa de seleção e pronto! A paginação padrão é uma opção atraente. Sua abordagem ingênua na recuperação de todos os registros, no entanto, o torna uma escolha implausível ao paginar quantidades suficientemente grandes de dados ou para sites com muitos usuários simultâneos. Em tais circunstâncias, devemos recorrer à paginação personalizada para fornecer um sistema responsivo.
O desafio da paginação personalizada é ser capaz de escrever uma consulta que retorne o conjunto preciso de registros necessários para uma página específica de dados. Felizmente, o Microsoft SQL Server 2005 fornece uma nova palavra-chave para classificar os resultados, o que nos permite escrever uma consulta que pode recuperar com eficiência o subconjunto adequado de registros. Neste tutorial, veremos como usar essa nova palavra-chave do SQL Server 2005 para implementar a paginação personalizada em um controle GridView. Embora a interface do usuário para paginação personalizada seja idêntica à da paginação padrão, passar de uma página para a próxima usando a paginação personalizada pode ser várias ordens de magnitude mais rápida do que a paginação padrão.
Observação
O ganho de desempenho exato exibido pela paginação personalizada depende do número total de registros que estão sendo paginados e da carga que está sendo colocada no servidor de banco de dados. No final deste tutorial, veremos algumas métricas aproximadas que mostram os benefícios em desempenho obtidos por meio da paginação personalizada.
Etapa 1: Entendendo o processo de paginação personalizada
Ao paginar dados, os registros precisos exibidos em uma página dependem da página de dados que está sendo solicitada e do número de registros exibidos por página. Por exemplo, imagine que quiséssemos percorrer os 81 produtos, exibindo 10 produtos por página. Ao visualizar a primeira página, queremos os produtos de 1 a 10; Ao visualizar a segunda página, estaríamos interessados nos produtos 11 a 20 e assim por diante.
Há três variáveis que determinam quais registros precisam ser recuperados e como a interface de paginação deve ser renderizada:
- Índice de Linha Inicial o índice da primeira linha na página de dados a ser exibida; esse índice pode ser calculado multiplicando o índice da página pelos registros a serem exibidos por página e adicionando um. Por exemplo, ao paginar os registros 10 por vez, para a primeira página (cujo índice de página é 0), o Índice de Linha Inicial é 0 * 10 + 1 ou 1; para a segunda página (cujo índice de página é 1), o Índice da Linha Inicial é 1 * 10 + 1 ou 11.
- Máximo de linhas : o número máximo de registros a serem exibidos por página. Essa variável é chamada de linhas máximas, pois para a última página pode haver menos registros retornados do que o tamanho da página. Por exemplo, ao paginar os 81 produtos 10 registros por página, a nona e última página terá apenas um registro. Nenhuma página, no entanto, mostrará mais registros do que o valor Máximo de Linhas.
- Contagem Total de Registros : o número total de registros que estão sendo paginados. Embora essa variável não seja necessária para determinar quais registros recuperar para uma determinada página, ela determina a interface de paginação. Por exemplo, se houver 81 produtos sendo paginados, a interface de paginação saberá exibir nove números de página na interface do usuário de paginação.
Com a paginação padrão, o Índice de Linha Inicial é calculado como o produto do índice de página e o tamanho da página mais um, enquanto o Máximo de Linhas é simplesmente o tamanho da página. Como a paginação padrão recupera todos os registros do banco de dados ao renderizar qualquer página de dados, o índice de cada linha é conhecido, tornando a mudança para a linha Índice de Linha Inicial uma tarefa trivial. Além disso, a Contagem Total de Registros está prontamente disponível, pois é simplesmente o número de registros na DataTable (ou qualquer objeto que esteja sendo usado para armazenar os resultados do banco de dados).
Dadas as variáveis Índice de Linha Inicial e Máximo de Linhas, uma implementação de paginação personalizada deve retornar apenas o subconjunto preciso de registros começando no Índice de Linha Inicial e até o número máximo de linhas de registros depois disso. A paginação personalizada oferece dois desafios:
- Devemos ser capazes de associar com eficiência um índice de linha a cada linha em todos os dados que estão sendo paginados para que possamos começar a retornar registros no Índice de Linha Inicial especificado
- Precisamos fornecer o número total de registros que estão sendo paginados
Nas próximas duas etapas, examinaremos o script SQL necessário para responder a esses dois desafios. Além do script SQL, também precisaremos implementar métodos na DAL e na BLL.
Etapa 2: Retornando o número total de registros que estão sendo paginados
Antes de examinarmos como recuperar o subconjunto preciso de registros para a página que está sendo exibida, vamos primeiro examinar como retornar o número total de registros que estão sendo paginados. Essas informações são necessárias para configurar corretamente a interface do usuário de paginação. O número total de registros retornados por uma consulta SQL específica pode ser obtido usando a COUNT
função agregada. Por exemplo, para determinar o número total de registros na Products
tabela, podemos usar a seguinte consulta:
SELECT COUNT(*)
FROM Products
Vamos adicionar um método ao nosso DAL que retorna essas informações. Em particular, criaremos um método DAL chamado TotalNumberOfProducts()
que executa a SELECT
instrução mostrada acima.
Comece abrindo o arquivo Conjunto de Dados Digitado Northwind.xsd
App_Code/DAL
na pasta. Em seguida, clique com o botão direito do ProductsTableAdapter
mouse no Designer e escolha Adicionar Consulta. Como vimos em tutoriais anteriores, isso nos permitirá adicionar um novo método à DAL que, quando invocado, executará uma instrução SQL ou procedimento armazenado específico. Assim como acontece com nossos métodos TableAdapter em tutoriais anteriores, para este, opte por usar uma instrução SQL ad hoc.
Figura 1: Usar uma instrução SQL ad hoc
Na próxima tela, podemos especificar que tipo de consulta criar. Como essa consulta retornará um único valor escalar, o número total de registros na Products
tabela, escolha a SELECT
opção que retorna um valor único.
Figura 2: Configurar a consulta para usar uma instrução SELECT que retorna um único valor
Depois de indicar o tipo de consulta a ser usada, devemos especificar a consulta.
Figura 3: Usar a consulta SELECT COUNT(*) FROM Products
Por fim, especifique o nome do método. Como mencionado anteriormente, vamos usar TotalNumberOfProducts
.
Figura 4: Nomear o método DAL TotalNumberOfProducts
Depois de clicar em Concluir, o assistente adicionará o TotalNumberOfProducts
método ao DAL. Os métodos escalares de retorno na DAL retornam tipos anuláveis, caso o resultado da consulta SQL seja NULL
. Nossa COUNT
consulta, no entanto, sempre retornará um não-valorNULL
; independentemente disso, o método DAL retorna um número inteiro anulável.
Além do método DAL, também precisamos de um método na BLL. Abra o arquivo de ProductsBLL
classe e adicione um TotalNumberOfProducts
método que simplesmente chama o método DAL TotalNumberOfProducts
:
public int TotalNumberOfProducts()
{
return Adapter.TotalNumberOfProducts().GetValueOrDefault();
}
O método DAL retorna TotalNumberOfProducts
um inteiro anulável; no entanto, criamos o ProductsBLL
método da classe para TotalNumberOfProducts
que ele retorne um inteiro padrão. Portanto, precisamos fazer com que o ProductsBLL
método da classe retorne TotalNumberOfProducts
a parte do valor do inteiro anulável retornado pelo método DAL TotalNumberOfProducts
. A chamada para GetValueOrDefault()
retorna o valor do inteiro anulável, se ele existir; se o inteiro anulável for null
, no entanto, ele retornará o valor inteiro padrão, 0.
Etapa 3: Retornando o subconjunto preciso de registros
Nossa próxima tarefa é criar métodos na DAL e na BLL que aceitem as variáveis Índice de Linha Inicial e Máximo de Linhas discutidas anteriormente e retornem os registros apropriados. Antes de fazermos isso, vamos primeiro examinar o script SQL necessário. O desafio que enfrentamos é que devemos ser capazes de atribuir com eficiência um índice a cada linha em todos os resultados que estão sendo paginados, para que possamos retornar apenas os registros começando no Índice de Linha Inicial (e até o número máximo de registros de registros).
Isso não é um desafio se já houver uma coluna na tabela do banco de dados que sirva como um índice de linha. À primeira vista, podemos pensar que o Products
campo da ProductID
tabela seria suficiente, já que o primeiro produto tem ProductID
de 1, o segundo de 2 e assim por diante. No entanto, a exclusão de um produto deixa uma lacuna na sequência, anulando essa abordagem.
Há duas técnicas gerais usadas para associar com eficiência um índice de linha aos dados a serem percorridos, permitindo assim que o subconjunto preciso de registros seja recuperado:
Usando a palavra-chave do SQL Server 2005 nova
ROW_NUMBER()
no SQL Server 2005, a palavra-chave associaROW_NUMBER()
uma classificação a cada registro retornado com base em alguma ordenação. Essa classificação pode ser usada como um índice de linha para cada linha.O uso de uma variável de tabela e
SET ROWCOUNT
a instrução doSET ROWCOUNT
SQL Server podem ser usados para especificar quantos registros totais uma consulta deve processar antes de encerrar; variáveis de tabela são variáveis T-SQL locais que podem conter dados tabulares, semelhantes a tabelas temporárias. Essa abordagem funciona igualmente bem com o Microsoft SQL Server 2005 e o SQL Server 2000 (enquanto a abordagem só funciona com oROW_NUMBER()
SQL Server 2005).A ideia aqui é criar uma variável de tabela que tenha uma
IDENTITY
coluna e colunas para as chaves primárias da tabela cujos dados estão sendo paginados. Em seguida, o conteúdo da tabela cujos dados estão sendo paginados é despejado na variável de tabela, associando assim um índice de linha sequencial (por meio daIDENTITY
coluna) para cada registro na tabela. Depois que a variável de tabela for preenchida, umaSELECT
instrução na variável de tabela, unida à tabela subjacente, poderá ser executada para extrair os registros específicos. ASET ROWCOUNT
instrução é usada para limitar de forma inteligente o número de registros que precisam ser despejados na variável de tabela.A eficiência dessa abordagem é baseada no número da página que está sendo solicitado, pois o
SET ROWCOUNT
valor é atribuído ao valor de Índice de Linha Inicial mais o Máximo de Linhas. Ao paginar páginas com números baixos, como as primeiras páginas de dados, essa abordagem é muito eficiente. No entanto, ele exibe um desempenho semelhante ao paginação padrão ao recuperar uma página perto do final.
Este tutorial implementa a paginação personalizada usando a ROW_NUMBER()
palavra-chave. Para obter mais informações sobre como usar a variável e SET ROWCOUNT
a técnica de tabela, consulte Paginação eficiente por meio de grandes quantidades de dados.
A ROW_NUMBER()
palavra-chave associou uma classificação a cada registro retornado em uma ordem específica usando a seguinte sintaxe:
SELECT columnList,
ROW_NUMBER() OVER(orderByClause)
FROM TableName
ROW_NUMBER()
Retorna um valor numérico que especifica a classificação de cada registro em relação à ordem indicada. Por exemplo, para ver a classificação de cada produto, ordenada do mais caro para o menos, poderíamos usar a seguinte consulta:
SELECT ProductName, UnitPrice,
ROW_NUMBER() OVER(ORDER BY UnitPrice DESC) AS PriceRank
FROM Products
A Figura 5 mostra os resultados dessa consulta quando executada por meio da janela de consulta no Visual Studio. Observe que os produtos são ordenados por preço, juntamente com uma classificação de preço para cada linha.
Figura 5: A classificação de preço é incluída para cada registro retornado
Observação
ROW_NUMBER()
é apenas uma das muitas novas funções de classificação disponíveis no SQL Server 2005. Para obter uma discussão mais completa sobre ROW_NUMBER()
o , juntamente com as outras funções de classificação, leia Retornando resultados classificados com o Microsoft SQL Server 2005.
Ao classificar os resultados pela coluna especificada ORDER BY
na OVER
cláusula (UnitPrice
, no exemplo acima), o SQL Server deve classificar os resultados. Essa é uma operação rápida se houver um índice clusterizado sobre a(s) coluna(s) pelas quais os resultados estão sendo ordenados ou se houver um índice de cobertura, mas pode ser mais caro de outra forma. Para ajudar a melhorar o desempenho de consultas suficientemente grandes, considere adicionar um índice não clusterizado para a coluna pela qual os resultados são ordenados. Consulte Funções de classificação e desempenho no SQL Server 2005 para obter uma visão mais detalhada das considerações de desempenho.
As informações de classificação retornadas por ROW_NUMBER()
não podem ser usadas diretamente na WHERE
cláusula. No entanto, uma tabela derivada pode ser usada para retornar o ROW_NUMBER()
resultado, que pode aparecer na WHERE
cláusula. Por exemplo, a consulta a seguir usa uma tabela derivada para retornar as colunas ProductName e UnitPrice, juntamente com o ROW_NUMBER()
resultado, e usa uma WHERE
cláusula para retornar apenas os produtos cuja classificação de preço está entre 11 e 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
Estendendo esse conceito um pouco mais, podemos utilizar essa abordagem para recuperar uma página específica de dados, considerando os valores desejados de Índice de Linha Inicial e Máximo de Linhas:
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>)
Observação
Como veremos mais adiante neste tutorial, o StartRowIndex
fornecido pelo ObjectDataSource é indexado a partir de zero, enquanto o ROW_NUMBER()
valor retornado pelo SQL Server 2005 é indexado a partir de 1. Portanto, a WHERE
cláusula retorna os registros em que PriceRank
é estritamente maior e StartRowIndex
menor ou igual a StartRowIndex
+ MaximumRows
.
Agora que discutimos como ROW_NUMBER()
pode ser usado para recuperar uma página específica de dados, considerando os valores Índice de Linha Inicial e Máximo de Linhas, agora precisamos implementar essa lógica como métodos na DAL e na BLL.
Ao criar esta consulta, devemos decidir a ordem pela qual os resultados serão classificados; Vamos classificar os produtos pelo nome em ordem alfabética. Isso significa que, com a implementação de paginação personalizada neste tutorial, não poderemos criar um relatório paginado personalizado que também possa ser classificado. No próximo tutorial, porém, veremos como essa funcionalidade pode ser fornecida.
Na seção anterior, criamos o método DAL como uma instrução SQL ad-hoc. Infelizmente, o analisador T-SQL no Visual Studio usado pelo assistente TableAdapter não gosta da OVER
sintaxe usada pela ROW_NUMBER()
função. Portanto, devemos criar esse método DAL como um procedimento armazenado. Selecione o Gerenciador de Servidores no menu Exibir (ou pressione Ctrl+Alt+S) e expanda o NORTHWND.MDF
nó. Para adicionar um novo procedimento armazenado, clique com o botão direito do mouse no nó Stored Procedures (Procedimentos armazenados) e escolha Add a New Stored Procedure (consulte a Figura 6).
Figura 6: Adicionar um novo procedimento armazenado para paginação por meio dos produtos
Esse procedimento armazenado deve aceitar dois parâmetros de entrada inteiros - @startRowIndex
e usar a ROW_NUMBER()
função ordenada pelo ProductName
campo, retornando apenas as linhas maiores que as especificadas @startRowIndex
e menores ou iguais a @startRowIndex
+ @maximumRow
@maximumRows
s. Insira o script a seguir no novo procedimento armazenado e clique no ícone Salvar para adicionar o procedimento armazenado ao banco de dados.
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)
Depois de criar o procedimento armazenado, reserve um momento para testá-lo. Clique com o botão direito do mouse no nome do GetProductsPaged
procedimento armazenado no Gerenciador de Servidores e escolha a opção Executar. Em seguida, @startRowIndex
o Visual Studio solicitará os parâmetros de entrada e @maximumRow
s (consulte a Figura 7). Experimente valores diferentes e examine os resultados.
@startRowIndex e @maximumRows parâmetros" />
Figura 7: Insira um valor para os @startRowIndex parâmetros e @maximumRows
Depois de escolher esses valores de parâmetros de entrada, a janela Saída mostrará os resultados. A Figura 8 mostra os resultados ao passar 10 para os @startRowIndex
parâmetros e @maximumRows
.
Figura 8: Os registros que apareceriam na segunda página de dados são retornados (clique para exibir a imagem em tamanho real)
Com esse procedimento armazenado criado, estamos prontos para criar o ProductsTableAdapter
método. Abra o Northwind.xsd
Conjunto de Dados Tipado, clique com o botão direito do ProductsTableAdapter
mouse no e escolha a opção Adicionar Consulta. Em vez de criar a consulta usando uma instrução SQL ad hoc, crie-a usando um procedimento armazenado existente.
Figura 9: Criar o método DAL usando um procedimento armazenado existente
Em seguida, somos solicitados a selecionar o procedimento armazenado a ser invocado. Escolha o GetProductsPaged
procedimento armazenado na lista suspensa.
Figura 10: Escolher o procedimento armazenado GetProductsPaged na lista suspensa
A próxima tela pergunta que tipo de dados é retornado pelo procedimento armazenado: dados tabulares, um único valor ou nenhum valor. Como o GetProductsPaged
procedimento armazenado pode retornar vários registros, indique que ele retorna dados tabulares.
Figura 11: Indicar que o procedimento armazenado retorna dados tabulares
Por fim, indique os nomes dos métodos que deseja criar. Assim como em nossos tutoriais anteriores, vá em frente e crie métodos usando Preencher uma DataTable e Retornar uma DataTable. Nomeie o primeiro método FillPaged
e o segundo GetProductsPaged
.
Figura 12: Nomear os métodos FillPaged e GetProductsPaged
Além de criar um método DAL para retornar uma página específica de produtos, também precisamos fornecer essa funcionalidade na BLL. Assim como o método DAL, o método GetProductsPaged da BLL deve aceitar duas entradas inteiras para especificar o Índice da Linha Inicial e o Máximo de Linhas e deve retornar apenas os registros que se enquadram no intervalo especificado. Crie esse método BLL na ProductsBLL classe que simplesmente chama o método GetProductsPaged da DAL, da seguinte forma:
[System.ComponentModel.DataObjectMethodAttribute(
System.ComponentModel.DataObjectMethodType.Select, false)]
public Northwind.ProductsDataTable GetProductsPaged(int startRowIndex, int maximumRows)
{
return Adapter.GetProductsPaged(startRowIndex, maximumRows);
}
Você pode usar qualquer nome para os parâmetros de entrada do método BLL, mas, como veremos em breve, optar por usar startRowIndex
e maximumRows
nos poupa de um pouco mais de trabalho ao configurar um ObjectDataSource para usar esse método.
Etapa 4: Configurando o ObjectDataSource para usar a paginação personalizada
Com os métodos BLL e DAL para acessar um subconjunto específico de registros concluídos, estamos prontos para criar um controle GridView que percorre seus registros subjacentes usando paginação personalizada. Comece abrindo a EfficientPaging.aspx
PagingAndSorting
página na pasta, adicione um GridView à página e configure-a para usar um novo controle ObjectDataSource. Em nossos tutoriais anteriores, geralmente tínhamos o ObjectDataSource configurado para usar o ProductsBLL
método da GetProducts
classe. Desta vez, no entanto, queremos usar o GetProductsPaged
método, pois o GetProducts
método retorna todos os produtos no banco de dados, enquanto GetProductsPaged
retorna apenas um subconjunto específico de registros.
Figura 13: Configurar o ObjectDataSource para usar o método GetProductsPaged da classe ProductsBLL
Como estamos criando um GridView somente leitura, reserve um momento para definir a lista suspensa de método nas guias INSERT, UPDATE e DELETE como (Nenhum).
Em seguida, o assistente ObjectDataSource nos solicita as fontes dos valores do GetProductsPaged
método e startRowIndex
maximumRows
dos parâmetros de entrada. Esses parâmetros de entrada serão definidos pelo GridView automaticamente, portanto, basta deixar a origem definida como Nenhum e clicar em Concluir.
Figura 14: Deixar as fontes de parâmetro de entrada como nenhuma
Depois de concluir o assistente ObjectDataSource, o GridView conterá um BoundField ou CheckBoxField para cada um dos campos de dados do produto. Sinta-se à vontade para personalizar a aparência do GridView como achar melhor. Optei por exibir apenas os ProductName
CategoryName
, , , SupplierName
QuantityPerUnit
, e UnitPrice
BoundFields. Além disso, configure o GridView para dar suporte à paginação marcando a caixa de seleção Habilitar Paginação em sua marca inteligente. Após essas alterações, a marcação declarativa GridView e ObjectDataSource deve ser semelhante à seguinte:
<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>
Se você visitar a página por meio de um navegador, no entanto, o GridView não será encontrado.
Figura 15: O GridView não é exibido
O GridView está ausente porque o ObjectDataSource está usando 0 como os valores para os parâmetros de GetProductsPaged
startRowIndex
entrada e maximumRows
. Portanto, a consulta SQL resultante não está retornando registros e, portanto, o GridView não é exibido.
Para remediar isso, precisamos configurar o ObjectDataSource para usar a paginação personalizada. Isso pode ser feito nas seguintes etapas:
- Defina a propriedade ObjectDataSource
EnablePaging
comotrue
isso indica ao ObjectDataSource que ele deve passar para osSelectMethod
dois parâmetros adicionais: um para especificar o Índice de Linha Inicial (StartRowIndexParameterName
) e outro para especificar o Máximo de Linhas (MaximumRowsParameterName
). - Definir o ObjectDataSource e as propriedades Da mesma forma, as propriedades e
MaximumRowsParameterName
indicam os nomes dos parâmetros de entrada passadosMaximumRowsParameterName
StartRowIndexParameterName
para finsSelectMethod
de paginaçãoStartRowIndexParameterName
personalizada. Por padrão, esses nomes de parâmetros sãostartIndexRow
emaximumRows
, e é por isso que, ao criar oGetProductsPaged
método na BLL, usei esses valores para os parâmetros de entrada. Se você optar por usar nomes de parâmetro diferentes para o método daGetProductsPaged
BLL, comostartIndex
emaxRows
, por exemplo, você precisará definir o ObjectDataSource sStartRowIndexParameterName
eMaximumRowsParameterName
as propriedades adequadamente (como startIndex paraStartRowIndexParameterName
e maxRows paraMaximumRowsParameterName
). - Defina a propriedade ObjectDataSource como
SelectCountMethod
o nome do método que retorna o número total de registros que estão sendo paginados (TotalNumberOfProducts
) lembre-se de que oProductsBLL
método da classe retornaTotalNumberOfProducts
o número total de registros que estão sendo paginados usando um método DAL que executa umaSELECT COUNT(*) FROM Products
consulta. Essas informações são necessárias para o ObjectDataSource para renderizar corretamente a interface de paginação. - Remova os
startRowIndex
elementos emaximumRows
<asp:Parameter>
da marcação declarativa do ObjectDataSource ao configurar o ObjectDataSource por meio do assistente, o Visual Studio adicionou automaticamente dois<asp:Parameter>
elementos para os parâmetros de entrada doGetProductsPaged
método. Ao definirEnablePaging
comotrue
, esses parâmetros serão passados automaticamente; se eles também aparecerem na sintaxe declarativa, o ObjectDataSource tentará passar quatro parâmetros para oGetProductsPaged
método e dois parâmetros para oTotalNumberOfProducts
método. Se você esquecer de remover esses<asp:Parameter>
elementos, ao visitar a página por meio de um navegador, receberá uma mensagem de erro como: ObjectDataSource 'ObjectDataSource1' não foi possível encontrar um método não genérico 'TotalNumberOfProducts' que tenha parâmetros: startRowIndex, maximumRows.
Depois de fazer essas alterações, a sintaxe declarativa do ObjectDataSource deve ser semelhante à seguinte:
<asp:ObjectDataSource ID="ObjectDataSource1" runat="server"
OldValuesParameterFormatString="original_{0}" TypeName="ProductsBLL"
SelectMethod="GetProductsPaged" EnablePaging="True"
SelectCountMethod="TotalNumberOfProducts">
</asp:ObjectDataSource>
Observe que as EnablePaging
propriedades e SelectCountMethod
foram definidas e os <asp:Parameter>
elementos foram removidos. A Figura 16 mostra uma captura de tela da janela Properties depois que essas mudanças forem feitas.
Figura 16: Para usar a paginação personalizada, configure o controle ObjectDataSource
Depois de fazer essas alterações, visite esta página por meio de um navegador. Você deve ver 10 produtos listados, ordenados em ordem alfabética. Reserve um momento para percorrer os dados, uma página de cada vez. Embora não haja diferença visual da perspectiva do usuário final entre a paginação padrão e a paginação personalizada, a paginação personalizada é mais eficiente para percorrer grandes quantidades de dados, pois recupera apenas os registros que precisam ser exibidos para uma determinada página.
Figura 17: Os dados, ordenados pelo nome do produto, são paginados usando paginação personalizada (clique para exibir a imagem em tamanho real)
Observação
Com a paginação personalizada, o valor da contagem de SelectCountMethod
páginas retornado pelo ObjectDataSource é armazenado no estado de exibição do GridView. Outras variáveis GridView, a PageIndex
coleção , EditIndex
, DataKeys
SelectedIndex
, e assim por diante, são armazenadas no estado de controle, que é mantido independentemente do valor da propriedade do GridViewEnableViewState
. Como o PageCount
valor é mantido em postbacks usando o estado de exibição, ao usar uma interface de paginação que inclui um link para levá-lo à última página, é imperativo que o estado de exibição do GridView seja habilitado. (Se sua interface de paginação não incluir um link direto para a última página, você poderá desativar o estado de exibição.)
Clicar no link da última página causa um postback e instrui o GridView a atualizar sua PageIndex
propriedade. Se o último link de página for clicado, o GridView atribuirá sua PageIndex
propriedade a um valor um a menos que sua PageCount
propriedade. Com o estado de exibição desabilitado, o PageCount
valor é perdido entre postbacks e o PageIndex
recebe o valor inteiro máximo. Em seguida, o GridView tenta determinar o índice da linha inicial multiplicando as PageSize
propriedades e PageCount
. Isso resulta em uma OverflowException
vez que o produto excede o tamanho inteiro máximo permitido.
Implementar paginação e classificação personalizadas
Nossa implementação de paginação personalizada atual exige que a ordem pela qual os dados são paginados seja especificada estaticamente ao criar o GetProductsPaged
procedimento armazenado. No entanto, você pode ter observado que a marca inteligente do GridView contém uma caixa de seleção Habilitar Classificação, além da opção Habilitar Paginação. Infelizmente, adicionar suporte de classificação ao GridView com nossa implementação de paginação personalizada atual classificará apenas os registros na página de dados exibida no momento. Por exemplo, se você configurar o GridView para também dar suporte à paginação e, ao exibir a primeira página de dados, classificar por nome de produto em ordem decrescente, ele inverterá a ordem dos produtos na página 1. Como mostra a Figura 18, isso mostra os Tigres de Carnarvon como o primeiro produto ao classificar em ordem alfabética inversa, o que ignora os outros 71 produtos que vêm depois dos Tigres de Carnarvon, em ordem alfabética; Somente os registros na primeira página são considerados na classificação.
Figura 18: Somente os dados mostrados na página atual são classificados (clique para exibir a imagem em tamanho completo)
A classificação só se aplica à página atual de dados porque a classificação está ocorrendo depois que os dados foram recuperados do método da GetProductsPaged
BLL e esse método retorna apenas esses registros para a página específica. Para implementar a classificação corretamente, precisamos passar a expressão sort para o GetProductsPaged
método para que os dados possam ser classificados adequadamente antes de retornar a página específica de dados. Veremos como fazer isso em nosso próximo tutorial.
Implementando paginação e exclusão personalizadas
Se você habilitar a funcionalidade de exclusão em um GridView cujos dados são paginados usando técnicas de paginação personalizadas, descobrirá que, ao excluir o último registro da última página, o GridView desaparece em vez de diminuir adequadamente o GridView PageIndex
. Para reproduzir esse bug, habilite a exclusão para o tutorial que acabamos de criar. Vá para a última página (página 9), onde você deve ver um único produto, pois estamos folheando 81 produtos, 10 produtos por vez. Exclua este produto.
Ao excluir o último produto, o GridView deve ir automaticamente para a oitava página, e essa funcionalidade é exibida com paginação padrão. Com a paginação personalizada, no entanto, depois de excluir o último produto na última página, o GridView simplesmente desaparece completamente da tela. O motivo exato pelo qual isso acontece está um pouco além do escopo deste tutorial; consulte Excluindo o último registro na última página de um GridView com paginação personalizada para obter os detalhes de baixo nível sobre a origem desse problema. Em resumo, é devido à seguinte sequência de etapas que são executadas pelo GridView quando o botão Excluir é clicado:
- Excluir o registro
- Obtenha os registros apropriados a serem exibidos para o especificado
PageIndex
ePageSize
- Verifique se o
PageIndex
não excede o número de páginas de dados na fonte de dados; se isso acontecer, diminua automaticamente a propriedade do GridViewPageIndex
- Associe a página apropriada de dados ao GridView usando os registros obtidos na Etapa 2
O problema decorre do fato de que na Etapa 2 o PageIndex
usado ao pegar os registros a serem exibidos ainda é o PageIndex
da última página cujo único registro acabou de ser excluído. Portanto, na Etapa 2, nenhum registro é retornado, pois essa última página de dados não contém mais nenhum registro. Em seguida, na Etapa 3, o GridView percebe que sua PageIndex
propriedade é maior que o número total de páginas na fonte de dados (já que excluímos o último registro na última página) e, portanto, diminui sua PageIndex
propriedade. Na Etapa 4, o GridView tenta se associar aos dados recuperados na Etapa 2; no entanto, na Etapa 2, nenhum registro foi retornado, resultando em um GridView vazio. Com a paginação padrão, esse problema não surge porque, na Etapa 2 , todos os registros são recuperados da fonte de dados.
Para corrigir isso, temos duas opções. A primeira é criar um manipulador de eventos para o manipulador de eventos do RowDeleted
GridView que determina quantos registros foram exibidos na página que acabou de ser excluída. Se houvesse apenas um registro, o registro que acabou de ser excluído deve ter sido o último e precisamos diminuir o GridView s PageIndex
. Obviamente, queremos atualizar apenas se a operação de exclusão foi realmente bem-sucedida, o PageIndex
que pode ser determinado garantindo que a e.Exception
propriedade seja null
.
Essa abordagem funciona porque atualiza a etapa 1, mas antes da PageIndex
etapa 2. Portanto, na Etapa 2, o conjunto apropriado de registros é retornado. Para fazer isso, use um código como o seguinte:
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);
}
Uma solução alternativa é criar um manipulador de eventos para o evento ObjectDataSource RowDeleted
e definir a AffectedRows
propriedade como um valor de 1. Depois de excluir o registro na Etapa 1 (mas antes de recuperar novamente os dados na Etapa 2), o GridView atualiza sua PageIndex
propriedade se uma ou mais linhas foram afetadas pela operação. No entanto, a AffectedRows
propriedade não é definida pelo ObjectDataSource e, portanto, essa etapa é omitida. Uma maneira de executar essa etapa é definir manualmente a AffectedRows
propriedade se a operação de exclusão for concluída com êxito. Isso pode ser feito usando um código como o seguinte:
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;
}
O código para ambos os manipuladores de eventos pode ser encontrado na classe code-behind do EfficientPaging.aspx
exemplo.
Comparando o desempenho da paginação padrão e personalizada
Como a paginação personalizada recupera apenas os registros necessários, enquanto a paginação padrão retorna todos os registros de cada página que está sendo exibida, fica claro que a paginação personalizada é mais eficiente do que a paginação padrão. Mas quão mais eficiente é a paginação personalizada? Que tipo de ganhos de desempenho podem ser vistos ao passar da paginação padrão para a paginação personalizada?
Infelizmente, não há uma resposta única aqui. O ganho de desempenho depende de vários fatores, sendo os dois mais proeminentes o número de registros que estão sendo paginados e a carga colocada no servidor de banco de dados e nos canais de comunicação entre o servidor Web e o servidor de banco de dados. Para tabelas pequenas com apenas algumas dezenas de registros, a diferença de desempenho pode ser insignificante. No entanto, para tabelas grandes, com milhares a centenas de milhares de linhas, a diferença de desempenho é aguda.
Um artigo meu, "Paginação personalizada no ASP.NET 2.0 com SQL Server 2005", contém alguns testes de desempenho que executei para exibir as diferenças de desempenho entre essas duas técnicas de paginação ao paginar por meio de uma tabela de banco de dados com 50.000 registros. Nesses testes, examinei o tempo para executar a consulta no nível do SQL Server (usando o SQL Profiler) e na página ASP.NET usando ASP.NET recursos de rastreamento. Lembre-se de que esses testes foram executados na minha caixa de desenvolvimento com um único usuário ativo e, portanto, não são científicos e não imitam os padrões típicos de carregamento do site. Independentemente disso, os resultados ilustram as diferenças relativas no tempo de execução para paginação padrão e personalizada ao trabalhar com quantidades suficientemente grandes de dados.
Duração média (seg) | Reads | |
---|---|---|
Paginação padrão do SQL Profiler | 1.411 | 383 |
Criador de perfil SQL de paginação personalizado | 0,002 | 29 |
Paginação padrão ASP.NET rastreamento | 2.379 | N/A |
Rastreamento de ASP.NET de paginação personalizado | 0.029 | N/A |
Como você pode ver, a recuperação de uma determinada página de dados exigiu 354 leituras a menos em média e concluídas em uma fração do tempo. Na página ASP.NET, a página personalizada foi capaz de renderizar em cerca de 1/100 do tempo que levou ao usar a paginação padrão.
Resumo
A paginação padrão é fácil de implementar, basta marcar a caixa de seleção Habilitar Paginação na marca inteligente do controle Web de dados, mas essa simplicidade tem o custo do desempenho. Com a paginação padrão, quando um usuário solicita qualquer página de dados , todos os registros são retornados, mesmo que apenas uma pequena fração deles possa ser mostrada. Para combater essa sobrecarga de desempenho, o ObjectDataSource oferece uma opção de paginação alternativa de paginação personalizada.
Embora a paginação personalizada melhore os problemas de desempenho da paginação padrão, recuperando apenas os registros que precisam ser exibidos, ela é mais complicada para implementar a paginação personalizada. Primeiro, uma consulta deve ser escrita que acesse corretamente (e com eficiência) o subconjunto específico de registros solicitados. Isso pode ser feito de várias maneiras; o que examinamos neste tutorial é usar a nova ROW_NUMBER()
função do SQL Server 2005 para classificar os resultados e, em seguida, retornar apenas os resultados cuja classificação está dentro de um intervalo especificado. Além disso, precisamos adicionar um meio para determinar o número total de registros que estão sendo paginados. Depois de criar esses métodos DAL e BLL, também precisamos configurar o ObjectDataSource para que ele possa determinar quantos registros totais estão sendo paginados e possa passar corretamente os valores Índice da Linha Inicial e Máximo de Linhas para a BLL.
Embora a implementação da paginação personalizada exija várias etapas e não seja tão simples quanto a paginação padrão, a paginação personalizada é uma necessidade ao paginar quantidades suficientemente grandes de dados. Como os resultados examinados mostraram, a paginação personalizada pode reduzir segundos do tempo de renderização da página ASP.NET e pode aliviar a carga no servidor de banco de dados em uma ou mais ordens de magnitude.
Boa programação!
Sobre o autor
Scott Mitchell, autor de sete livros ASP/ASP.NET e fundador da 4GuysFromRolla.com, trabalha com tecnologias da Web da Microsoft desde 1998. Scott trabalha como consultor, instrutor e escritor independente. Seu último livro é Sams Teach Yourself ASP.NET 2.0 em 24 horas. Ele pode ser contatado em mitchell@4GuysFromRolla.com. ou através de seu blog, que pode ser encontrado em http://ScottOnWriting.NET.