anterior ou próximo. O aparecimento e desaparecimento das tabelas criam a ilusão que apenas uma tabela é usada para percorrer (paginar) os registros.
Listagem 1 PagingTable.html
| Page 1 |
Page 1 |
Page 1 |
| Page 1 |
Page 1 |
Page 1 |
| Page 1 |
Page 1 |
Page 1 |
| Page 2 |
Page 2 |
Page 2 |
| Page 2 |
Page 2 |
Page 2 |
| Page 2 |
Page 2 |
Page 2 |
| Page 3 |
Page 3 |
Page 3 |
| Page 3 |
Page 3 |
Page 3 |
| Page 3 |
Page 3 |
Page 3 |
Prev
Next
Figura 1 Tabela HTML paginável
A mesma técnica pode ser usada para criar DataGrids que suportem paginação no lado cliente. O desafio é modificar a classe DataGrid de modo a produzir uma saída semelhante à da Listagem 1. Caso você não tenha adivinhado, é exatamente disso que este artigo: melhorar o controle DataGrid ASP.NET por meio da criação de um plug-in que efetue paginações no cliente. Além de adicionar uma nova ferramenta útil à sua toolbox, este é um exemplo maravilhoso de como modificar controles ASP.NET, derivando a partir deles e adicionando funcionalidade por conta própria.
Introduzindo o controle ClientPageDataGrid
ClientPageDataGrid é um classe derivada do DataGrid que adiciona suporte à paginação no lado cliente ao ASP.NET. Como ela é uma substituta funcional do DataGrid, você pode usá-la da mesma maneira que usaria um DataGrid. A paginação no lado cliente é totalmente gratuita e pode inclusive ser desativada.
A página da Web na Listagem 2 faz a paginação de maneira normal: efetuando postbacks repetidamente para o servidor. Ela armazena um DataGrid que é configurado para paginar sua saída (AllowPaging="true") e mostrar 16 registros por página (PageSize="16"). Clicar em uma das setas no paginador na parte inferior da grade gera um postback para o servidor e aciona um evento PageIndexChanged, ativando o método OnNewPage. O método OnNewPage define a propriedade CurrentPageIndex do DataGrid para o índice da página anterior ou próxima, fazendo assim com que essa página seja processada de volta para o cliente.
Listagem 2 ServerPaging.aspx
<%@ Import Namespace="System.Data" %>
<%@ Import Namespace="System.Data.SqlClient" %>
AutoGenerateColumns="false" CellPadding="2"
Width="100%" Font-Name="Verdana" Font-Size="8pt"
AllowPaging="true"
PageSize="16" OnPageIndexChanged="OnNewPage">
<COLUMNS>
DataField="productid" ItemStyle-
HorizontalAlign="center" />
DataField="productname" />
DataField="unitsinstock" />
</COLUMNS>
<HEADERSTYLE </FONT>
Font-Bold="true" HorizontalAlign="center" />
<ALTERNATINGITEMSTYLE BackColor="beige" />
void Page_Load (Object sender, EventArgs e)
{
if (!IsPostBack)
BindDataGrid ();
}
void OnNewPage (Object sender, DataGridPageChangedEventArgs e)
{
MyDataGrid.CurrentPageIndex = e.NewPageIndex;
BindDataGrid ();
}
void BindDataGrid ()
{
SqlDataAdapter adapter = new SqlDataAdapter
("select productid, productname, unitsinstock
from products",
"server=localhost;database=northwind;
Integrated Security=SSPI");
DataSet ds = new DataSet ();
adapter.Fill (ds);
MyDataGrid.DataSource = ds;
MyDataGrid.DataBind ();
}
A Listagem 3 mostra a mesma página implementada com ClientPageDataGrid, com as alterações mostradas em vermelho. O controle DataGrid foi substituído pelo ClientPageDataGrid. AllowPaging="true" foi alterado para AllowClientPaging="true" para ativar a paginação no lado cliente, e PageSize="16" agora mostra ClientPageSize="16". Tanto o atributo OnPageIndexChanged como o método OnNewPage foram excluídos. Nenhum deles é necessário agora, já que a paginação será manipulada totalmente no cliente.
Listagem 3 ClientPaging.aspx
<%@ Import Namespace="System.Data" %>
<%@ Import Namespace="System.Data.SqlClient" %>
<%@ Register TagPrefix="msdn" Namespace="MSDN"
Assembly="ClientPageDataGrid" %>
<msdn:ClientPageDataGrid RunAt="server" ID="MyDataGrid"
AutoGenerateColumns="false" CellPadding="2" Width="100%"
Font-Name="Verdana" Font-Size="8pt"
ClientPageSize="16" AllowClientPaging="true">
<COLUMNS>
DataField="productid" ItemStyle-
HorizontalAlign="center" />
DataField="productname" />
DataField="unitsinstock" />
</COLUMNS>
<HEADERSTYLE </FONT>
Font-Bold="true" HorizontalAlign="center" />
<ALTERNATINGITEMSTYLE BackColor="beige" />
void Page_Load (Object sender, EventArgs e)
{
if (!IsPostBack)
BindDataGrid ();
}
void BindDataGrid ()
{
SqlDataAdapter adapter = new SqlDataAdapter
("select productid, productname, unitsinstock
from products",
"server=localhost;database=northwind;
Integrated Security=SSPI");
DataSet ds = new DataSet ();
adapter.Fill (ds);
MyDataGrid.DataSource = ds;
MyDataGrid.DataBind ();
}
Para fazer um teste com o controle ClientPageDataGrid, copie ClientPaging.aspx para um diretório virtual (por exemplo, wwwroot) em seu servidor Web ASP.NET. Em seguida, copie o controle assembly—ClientPageDataGrid.dll — no subdiretório bin do diretório virtual e abra ClientPaging.aspx em seu navegador. Você verá uma página semelhante a mostrada na Figura 2. Use as setas para paginar para a frente e para trás. ClientPaging.aspx se parece exatamente com ServerPaging.aspx, mas por baixo dessa aparência ela é bem diferente. ServerPaging.aspx coloca mais carga no servidor Web e só pode paginar na velocidade permitida pela conexão entre o cliente e o servidor. ClientPaging.aspx, por outro lado, leva um pouco mais de tempo para carregar porque obtém todos os registros, em vez de apenas uma página, mas uma vez carregada permitirá uma paginação bastante ágil, independentemente da velocidade da conexão.
Figura 2 ClientPageDataGrid
A interface programática do ClientPageDataGrid consiste em quatro propriedades públicas que ele adiciona àquelas herdadas do DataGrid (consulte a Tabela 1). AllowClientPaging ativa e desativa a paginação. O padrão é true, então você pode omitir AllowClientPaging="true" nas tags de ClientPageDataGrid, se desejar. ClientPageSize controla o número de registros mostrados em cada página, enquanto ClientPageCount recupera a contagem de página. ClientCurrentPageIndex determina qual página está sendo exibida no momento. O método Page_Load a seguir configura o ClientPageDataGrid denominado "MyDataGrid" para mostrar inicialmente a Page 2 (cujo índice baseado em zero é 1), em vez da Page 1:
void Page_Load (Object sender, EventArgs e)
{
if (!IsPostBack) {
BindDataGrid ();
MyDataGrid.ClientCurrentPageIndex = 1;
}
}
Quando a página que contém um ClientPageDataGrid faz um post de volta ao servidor, a propriedade ClientCurrentPageIndex é atualizada para indicar qual página estava em exibição no cliente quando o postback ocorreu. Entre os postbacks, o servidor não sabe qual página o usuário está visualizando.
Tabela 1 Propriedades Public de ClientPageDataGrid
|
Nome da propriedade |
Descrição |
Tipo |
Leitura |
Gravação |
Padrão |
|
AllowClientPaging |
Ativa e desativa paginação no lado cliente |
bool |
|
|
True |
|
ClientPageSize |
Obtém ou define o número de registros exibidos por página |
Int |
|
|
10 |
|
ClientPageCount |
Obtém a contagem de página atual |
Int |
|
|
N/D |
|
ClientCurrentPageIndex |
Obtém ou define o índice da página atual |
Int |
|
|
0 |
Por dentro do ClientPageDataGrid
A habilidade de o ClientPageDataGrid gerar tabelas HTML paginadas começa com o método Render, que é incluído na Listagem 4. Render consiste em um método virtual que todos os server controls herdam de System.Web.UI.Control. O método Render é chamado pelo Microsoft® .NET Framework para transformar (render) um controle em HTML cada vez que a página que aloja o controle é solicitada.
Listagem 4 Método Render de ClientPageDataGrid
protected override void Render (HtmlTextWriter writer)
{
if (AllowClientPaging && Items.Count > 0) {
for (int i=0; i<CLIENTPAGECOUNT; i++) {</FONT>
// Gerar uma tag
writer.AddAttribute (HtmlTextWriterAttribute.Id,
ClientID + "_page" + i.ToString ());
writer.AddAttribute
(HtmlTextWriterAttribute.Style,
i == ClientCurrentPageIndex ?
"display: block" : "display: none");
writer.RenderBeginTag (HtmlTextWriterTag.Div);
// Processar uma página dentro do
ShowItems (i * ClientPageSize,
Math.Min ((i * ClientPageSize) +
ClientPageSize - 1,
Items.Count - 1));
UpdatePager (i);
base.Render (writer);
// Gerar uma tag
writer.RenderEndTag ();
}
// Restaurar todos os itens DataGrid para o
// estado visível
ShowItems (0, Items.Count - 1);
}
else {
base.Render (writer);
}
}
ClientPageDataGrid sobrecarrega o método Render do DataGrid e chama a implementação da classe base não apenas uma vez, mas várias. Especificamente, ClientPageDataGrid chama DataGrid.Render uma vez a cada página gerada e encapsula a saída em elementos
, posicionando as chamadas para base.Render entre as chamadas para RenderBeginTag e RenderEndTag. Antes de chamar o método Render da classe base (superclasse), ClientPageDataGrid.Render invoca um método local denominado ShowItems para ocultar as linhas que não aparecem na página processada no momento atual, definindo as propriedades Visible dessas linhas como false.
No caso de um ClientPageDataGrid configurado para exibir 16 linhas por página, o resultado será a Page 1 formada de um
que contém os primeiros 16 itens do grid, a Page 2 formada de um
que contém os próximos 16 itens, e assim por diante. Todos os elementos
(exceto aquele que representa a página visível) incluem atributos que impedem que eles sejam vistos pelo navegador. Somente o
que representa a página atual — a página cujo índice é igual a ClientCurrentPageIndex —é processado com um atributo que o torna visível ao usuário (veja a
Listagem 5).
Listagem 5 HTML Gerado por ClientPageDataGrid
Cada "página" processada por ClientPageDataGrid inclui um paginador que está presente no DataGrid porque ClientPageDataGrid sobrecarrega o método DataBind do DataGrid e o chama com AllowPaging definido como true e PageSize definido como o número total de itens na fonte de dados. Grande parte do código em ClientPageDataGrid.cs destina-se a fazer com que o paginador funcione e a garantir que ele ofereça suporte tanto a paginadores de estilo previous/next (<PAGERSTYLE Mode="NextPrev">) como a paginadores numéricos (<PAGERSTYLE Mode="NumericPages">).
Um paginador DataGrid convencional contém LinkButtons que geram post de volta ao servidor e disparam eventos PageIndexChanged. ClientPageDataGrid substitui esses LinkButtons por hyperlinks que apontam para funções JavaScript na página do cliente. O método UpdatePager (veja a Listagem 6), que é chamado um pouco antes de Render chamar o método Render da superclasse, faz a substituição. Para localizar o paginador, UpdatePager verifica o DataGrid em busca de uma linha cujo tipo seja ListItemType.Pager. Em seguida, ele exclui os controles do paginador e adiciona seus próprios controles. Veja um exemplo do HTML gerado por um controle DataGrid convencional quando ele processa um paginador:
doPostBack('MyDataGrid$_ctl20$_ctl0','')">
<
_doPostBack('MyDataGrid$_ctl20$_ctl1','')">
>
|
Veja agora o mesmo paginador processado por um controle ClientPageDataGrid:
_onPrevPage ('MyDataGrid');"><
__onNextPage ('MyDataGrid');">>
|
As funções JavaScript referenciadas nas tags anchor são registradas quando ClientPageDataGrid.OnPreRender chama Page.RegisterClientScriptBlock. Chamar RegisterClientScriptBlock assegura que o bloco de scripts que contém as funções seja incluído na saída apenas uma vez, mesmo que a página contenha várias instâncias do controle ClientPageDataGrid.
Embora o paginador ClientPageDataGrid por si só não induza nenhum postback, é possível que outros controles na página o façam — e provavelmente o farão. Isso apresenta dois desafios. Primeiro, é necessário atualizar ClientCurrentPageIndex quando ocorrer um postback, a fim de que o código no lado servidor possa determinar qual página estava sendo exibida no momento do postback. Segundo, o ClientPageDataGrid submetido de volta ao cliente após o postback deve preservar a página atual. Em outras palavras, se Page 3 for exibida durante o postback da página para o servidor, os códigos de renderização de ClientPageDataGrid devem anexar um atributo ao elemento
que representa a Page 3, em vez de no elemento
que representa a Page 1.
Para solucionar esses desafios, ClientPageDataGrid registra um campo hidden denominado controlid__ PAGENUM:
Page.RegisterHiddenField (ClientID + "__PAGENUM",
ClientCurrentPageIndex.ToString ());
Listagem 6 Método UpdatePager
void UpdatePager (int page)
{
// Obter uma referência ao paginador
TableCell pager = null;
for (int i = Controls[0].Controls.Count - 1; i >=0; i—) {
if (((DataGridItem) Controls[0].Controls[i]).ItemType ==
ListItemType.Pager)
{
pager = (TableCell) Controls[0].Controls[i].Controls[0];
break;
}
}
if (PagerStyle.Mode == PagerMode.NextPrev)
{
// Implementar um paginador Next-Prev
// no lado cliente
pager.Controls.Clear ();
pager.Controls.Add (CreatePrevPageControl (page));
pager.Controls.Add (new LiteralControl
(" "));
pager.Controls.Add (CreateNextPageControl (page));
}
else if (PagerStyle.Mode == PagerMode.NumericPages)
{
pager.Controls.Clear ();
if (ClientPageCount <= PagerStyle.PageButtonCount)
{
// Implementar um paginador numérico no
// estilo "1 2 3"
for (int i=0; i<CLIENTPAGECOUNT i++) {</FONT>
pager.Controls.Add (CreatePageControl
(page, i));
pager.Controls.Add (new LiteralControl
(" "));
}
pager.Controls.Add (CreatePageControl (page,
ClientPageCount - 1));
}
else
{
// Implementar um paginador numérico de
// estilo "... 1 2 3 ..."
int sections = (ClientPageCount +
PagerStyle.PageButtonCount - 1) /
PagerStyle.PageButtonCount;
int section = page / PagerStyle.PageButtonCount;
// Adicionar um "..." inicial se não for a
// primeira seção
if (section > 0)
{
pager.Controls.Add (CreatePrevSectionControl
(page, section));
pager.Controls.Add (new LiteralControl
(" "));
}
// Adicionar números de página
int start = section * PagerStyle.PageButtonCount;
int end = Math.Min (start +
PagerStyle.PageButtonCount - 1,
ClientPageCount - 1);
for (int i=start; i<END; i++) {</FONT>
pager.Controls.Add (CreatePageControl
(page, i));
pager.Controls.Add (new LiteralControl
(" "));
}
pager.Controls.Add (CreatePageControl (page,
end));
// Adicionar "..." posterior se não for a
// seção final
if (section < sections - 1) {
pager.Controls.Add (new LiteralControl
(" "));
pager.Controls.Add (CreateNextSectionControl
(page, section));
}
}
}
}
As funções JavaScript que paginam a saída no cliente atualizam os campos hidden cada vez que a página atual é alterada. Quando a página faz o post de volta ao servidor, o valor do campo hidden é incluído nos dados do postback. O método LoadViewState do ClientPageDataGrid lê o valor nos dados do postback e o atribui à ClientCurrentPageIndex por meio de uma chamada a um método local denominado RestoreCurrentPageIndex, que faz o seguinte:
string page = Page.Request[ClientID + "__PAGENUM"];
•••
ClientCurrentPageIndex = Convert.ToInt32 (page);
A partir da leitura da propriedade ClientCurrentPageIndex, o event handler do postback pode agora determinar qual página o usuário estava visualizando. E, como o ClientPageDataGrid.Render usa ClientCurrentPageIndex para determinar qual
deverá estar visível, a página que estava visível antes do postback ainda estará visível após o postback.
Restaurar o índice da página atual a partir dos dados de postback em LoadViewState tem algumas vantagens. Primeiro, não é possível definir o índice da página atual até que o controle seja preenchido com itens, e é em LoadViewState que o controle é preenchido após um postback. Além disso, como LoadViewState é chamado antes de a página disparar um evento Load, o método Page_Load pode ler ClientCurrentPageIndex para descrobrir qual página o usuário estava visualizando quando ocorreu o postback. Isso pode ser importante porque o índice da página atual poderia ser usado para afetar outras alterações na página.
RestoreCurrentPageIndex também é chamado pelo método DataBind de ClientPageDataGrid, mas apenas se não tiver sido chamado a partir de LoadViewState. Por quê? Porque se o view state estiver desabilitado (ou seja, se a propriedade EnableViewState do controle for false), LoadViewState não será chamado pelo runtime do ASP.NET. Se view state estiver desabilitado, a página chamará DataBind para preencher novamente o controle, o que significa que o método DataBind é o local mais apropriado para fazer uma segunda tentativa de restaurar o índice de página atual a partir dos dados do postback.
Você deve ter percebido que o método DataBind do ClientPageDataGrid vincula à fonte de dados não apenas uma vez, mas duas (veja a Listagem 7). A primeira chamada para o método DataBind da classe base (superclasse) fornece uma contagem de registro ao ClientPageDataGrid. A segunda chamada - feita com AllowPaging definido como true e PageSize definido para o record count - produz um DataGrid que contém todos os registros da fonte de dados com um paginador no canto inferior. Esse DataGrid está localizado logo abaixo da superfície de ClientPageDataGrid e funciona como a fonte de toda a renderização.
Outro aspecto incomum do método DataBind do ClientPageDataGrid é que ele converte DataReaders em DataSets. São duas as razões para isso. Primeiro, só é possível vincular a um DataReader uma vez, já que ele é uma fonte de dados forward-only. Segundo, normalmente os DataGrids não suportam DataReaders como fontes de dados se AllowPaging for true, a não ser que AllowCustomPaging também seja true. Converter internamente DataReaders em DataSets resolve os dois problemas de forma organizada e assegura que o ClientPageDataGrid funcione perfeitamente com DataReaders (como faz com DataSets).
Listagem 7 Método DataBind
public override void DataBind ()
{
if (AllowClientPaging && DataSource != null) {
object OriginalDataSource = null;
// Se a fonte de dados for um DataReader,
// convertê-la em um DataSet
if (DataSource is IDataReader) {
OriginalDataSource = DataSource;
DataSource = DataSetFromDataReader
((IDataReader) DataSource);
}
// Vincular uma vez com a paginação desativada
// (e os eventos ItemCreated e ItemDataBound
// suprimidos) para obter uma contagem de item
_SuppressEvents = true;
AllowPaging = false;
base.DataBind ();
_SuppressEvents = false;
// Vincular novamente com a paginação ativada
// para criar um DataGrid contendo todos os
// itens com um paginador na parte inferior
PageSize = Items.Count;
AllowPaging = true;
base.DataBind ();
// Restaurar a fonte de dados original
if (OriginalDataSource != null)
DataSource = OriginalDataSource;
// Restaurar o índice de página atual a partir
// de dados de postback se ele ainda não tiver
// sido restaurado
if (Page.IsPostBack && AllowClientPaging &&
!_RestoreCurrentPageIndexCalled)
RestoreCurrentPageIndex ();
}
else {
base.DataBind ();
}
}
Advertências
Existe alguma desvantagem em paginar no lado cliente? Pode apostar que sim. Quanto maior o recordset, mais lento será o carregamento da página, porque a resposta HTTP retornará todos os registros, e não apenas os contidos na página atual. No caso de recordsets relativamente pequenos — digamos, com 1.000 registros ou menos — você provavelmente não se incomodará. No entanto, nos recordsets muito grandes, o tempo de carregamento poderá ser inaceitavelmente alto.
Um fator adicional a ser considerado quando se trata de utilização de largura de banda é a propensão do DataGrid a serializar seus conteúdos em um view state. Os DataGrids maiores aumentam o tempo de download de duas maneiras. Em primeiro lugar, eles geram tabelas HTML grandes. Segundo, eles aumentam o tamanho do view-state. Como o ASP.NET persiste o view state usando um campo hidden, os conteúdos do DataGrid aparecerão essencialmente uma vez em cada solicitação HTTP e duas vezes em cada resposta HTTP. Essa é mais uma razão para manter a contagem do registro pequena e recorrer à paginação no servidor para os recordsets maiores. Se você precisar usar um ClientPageDataGrid para paginar um número grande de registros, considere configurar a propriedade EnableViewState do controle como false e vincule à fonte de dados em cada solicitação de página (em vez de só vincular quando Page.IsPostBack for false). Isso poderia diminuir até dois terços do tamanho da resposta.
Uma última coisa a ser lembrada em relação ao ClientPageDataGrid é que, devido à maneira como ele processa página individuais, você provavelmente desejará tornar o ClientPageSize um número par se usar a propriedade AlternatingItemStyle para processar itens de número ímpar de maneira diferente dos itens de número par. Caso contrário, Page 1 terá um item não-alternante no canto superior, Page 2 terá um item alternante, e assim por diante. O usuário vai se perguntar por que o formato da tabela é alterado a cada vez que ele muda para a próxima página ou para a página anterior.
Conclusão
A classe DataGrid é um ótimo exemplo de um dos recursos mais irresistíveis do ASP.NET: sua capacidade de ocultar lógica comportamental e renderização complexas em classes reutilizáveis. ClientPageDataGrid aperfeiçoa ainda mais a noção de controles server-side e demonstra não apenas como modificar tipos de controle predefinidos de acordo com suas necessidades de trabalho, mas também como estendê-los para adicionar funcionalidade no lado cliente. Controles de servidor que recorrem a scripts no lado cliente para funcionarem de forma mais eficaz são uma idéia que está virando realidade. Espero que os autores de controle concordem comigo.