Este artículo describe una técnica usando WebBroker con Delphi para mostrar una tabla en una página web de a varios registros cada vez, con enlaces a la página previa o siguiente según corresponda.

Paginación en WebBroker usando ClientDatasets

Copyright © 2002 Ing. Ernesto Cullen

JfControls Library - para Delphi y C++ Builder

Introducción

Este artículo describe una técnica para mostrar una tabla en una página web, RecsPerPage (una variable privada) registros cada vez, con enlaces a la página previa o siguiente, si es posible. La siguiente figura muestra la primera página cuando RecsPerPage vale 5:

¿Por qué usar un ClientDataset para esto? El componente ClientDataset permite traer a la memoria un conjunto reducido de registros cada vez, no importa cuán grande sea el resultado de la consulta. Y se pueden usar con cualquier tecnología de acceso que provea un descendiente de TDataset (BDE, ADO, IBX, DBExpress, etc). Además, tenemos otras ventajas: inversión inmediata del orden usando índices, campos de agregación, etc.

En este ejemplo utilizo la tabla Biolife.db de los demos que vienen con Delphi (DBDemos) accediendo a través de la BDE.

Usando una directiva de compilación condicional logramos que este ejemplo funcione tanto en Delphi 5 como 6. También debería funcionar en Kylix (usando otra tecnologia de acceso, claro está), aunque no lo he probado.

Funcionamiento

En pocas palabras, este ejemplo:

  • Muestra una primera página con los primeros registros de la tabla y un enlace 'Siguiente' para ir a la siguiente página
  • A partir de ese momento:
    • Si se sigue un enlace, se ejecuta la misma acción en el programa pero esta vez el SQL es generado para que recupere los registros que siguen al último de la página anterior (si vamos hacia delante) o los anteriores al primero (si vamos hacia atrás).
    • Al mismo tiempo que la tabla es generada, se guardan los valores del campo clave de ordenación del primero y del último registro mostrado.
    • A continuación de la tabla se generan dos formularios HTML ('formularios de acción') con dos campos ocultos cada uno: el primero con el valor del primer registro o del último –según el formulario- y el segundo con la dirección del movimiento. En una aplicación real habría aquí por lo menos un campo más, con el ID de conexión.
    • Como una ayuda para la depuración, se muestran luego los valores primero y último de esta página
    • Finalmente, se agrega un enlace para mostrar la página siguiente y otro para la anterior, si hay registros, o un texto indicando que se ha alcanzado el límite inferior o superior de la tabla.

En este ejemplo he usado el método GET en los 'formularios de acción' para que podamos ver los valores pasados al servidor en cada página; se puede cambiar por POST sin otros cambios, y no se verán los valores en el browser.

Desarrollo del ejemplo

Primero lo primero: cree una aplicación WebServer de cualquier tipo. Agregue una acción al WebModule y márquela como Acción por Defecto (Default = True). Ahora coloque un componente DatasetTableProducer y un PageProducer de la página Internet de la paleta de componentes.

Para acceder a la tabla, agregue los siguientes componentes:

  • TDatabase
  • TSession
  • TQuery
  • TDatasetProvider
  • TClientDataset

En la figura se ve el módulo completo (note los cambios de nombre de algunos componentes).

Asumiré que sabe como conectar los componentes para acceder a la tabla 'Biolife.db' en el directorio DBDemos; no olvide poner Session1.AutoSessionName:= true. Si no puede conectar, revise los valores de las propiedades en el siguiente listado textual del módulo.

object WebModule1: TWebModule1
  OldCreateOrder = False
  OnCreate = WebModuleCreate
  Actions = <
    item
      Default = True
      Name = 'WebActionItem1'
      PathInfo = '/'
      Producer = PageProducer1
    end>
  Left = 628
  Top = 119
  Height = 207
  Width = 229
  object cds: TClientDataSet
    Aggregates = <>
    FieldDefs = <
      item
        Name = 'Species No'
        DataType = ftFloat
      end
      item
        Name = 'Category'
        DataType = ftString
        Size = 15
      end
      item
        Name = 'Common_Name'
        DataType = ftString
        Size = 30
      end
      item
        Name = 'Species Name'
        DataType = ftString
        Size = 40
      end
      item
        Name = 'Length (cm)'
        DataType = ftFloat
      end
      item
        Name = 'Length_In'
        DataType = ftFloat
      end
      item
        Name = 'Notes'
        DataType = ftMemo
        Size = 50
      end
      item
        Name = 'Graphic'
        DataType = ftGraphic
      end>
    IndexDefs = <
      item
        Name = 'ixInverted'
        Fields = 'species no'
      end>
    PacketRecords = 21
    Params = <>
    ProviderName = 'dsp1'
    StoreDefs = True
    Left = 28
    Top = 16
  end
 
object WebModule1: TWebModule1
  object TableProducer: TDataSetTableProducer
    Caption = 'Animals'
    DataSet = cds
    OnCreateContent = TableProducerCreateContent
    OnFormatCell = TableProducerFormatCell
    Left = 92
    Top = 18
  end
  object Query1: TQuery
    DatabaseName = 'demosDB'
    SessionName = 'Session1_1'
    SQL.Strings = (
      'select *'
      'from biolife')
    UniDirectional = True
    Left = 28
    Top = 70
  end
  object Session1: TSession
    Active = True
    AutoSessionName = True
    NetFileDir = 'C:\'
    Left = 92
    Top = 70
  end
  object Database1: TDatabase
    AliasName = 'DBDEMOS'
    DatabaseName = 'demosDB'
    KeepConnection = False
    LoginPrompt = False
    SessionName = 'Session1_1'
    Left = 156
    Top = 70
  end
  object dsp1: TDataSetProvider
    DataSet = Query1
    Constraints = True
    Options = [poAutoRefresh]
    UpdateMode = upWhereKeyOnly
    Left = 156
    Top = 18
  end
  object PageProducer1: TPageProducer
    HTMLDoc.Strings = (
      '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">'
      '<HTML>'
      '<HEAD>'
      '<TITLE> Paging demo </TITLE>'
      '</HEAD>'
      '<BODY>'
      '<#table>'
      '<BR>'
      '<#PageDn>&nbsp;&nbsp;&nbsp;
        &nbsp;&nbsp;&nbsp;<#PageUp>'
      '</BODY>'
      '</HTML>')
    OnHTMLTag = PageProducer1HTMLTag
    Left = 92
    Top = 124
  end
end

Ahora decimos a la acción Action1 que su productor de contenido web es el PageProducer1, poniendo Action1.Producer:= PageProducer1. Este componente actuará como un 'controlador de producción de contenido', llamando al DatasetTableProducer cuando necesite la tabla con los registros.

El modelo de contenido a producir está escrito directamente en la propiedad HTMLDoc del PageProducer1:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<HTML>
<HEAD>
<TITLE> Paging demo </TITLE>
</HEAD>
<BODY>
<#table>
<BR>
<#PageDn>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<#PageUp>
</BODY>
</HTML>

Como podemos ver, hay tres etiquetas transparentes (table, PageDn y PageUp). Es responsabilidad del componente PageProducer1 el reemplazar éstas con contenido real, de la siguiente manera:

  • Table se reemplaza por una tabla con RecsPerPage registros, generada por el DatasetTableProducer.
  • PageDn se reemplaza con el enlace a la página anterior, o un texto si no hay registros antes del primero que estamos mostrando.
  • PageUp se reemplaza por el enlace a la página siguiente, o un texto si no hay más registros.

Esto se hace en el evento OnHTMLTag del PageProducer1:

procedure TWebModule1.PageProducer1HTMLTag(Sender: TObject; Tag: TTag;
  const TagString: String; TagParams: TStrings; var ReplaceText: String);
begin
  if SameText('table',TagString) then
    ReplaceText:= TableProducer.Content+FormPrev+FormNext+ShowValues 
    //ShowValues is for debug only
  else
  if SameText('PageDn',TagString) then
    if FPrev then
      ReplaceText:= '<a href="javascript:formprev.submit();">&lt;&lt; Previous</a>'
    else
      ReplaceText:= 'First record shown'
  else
  if SameText('PageUp',TagString) then
    if FNext then
      ReplaceText:= '<a href="javascript:formnext.submit();">Next &gt;&gt;</a>'
    else
      ReplaceText:= 'Last record shown'
end;

El código es bastante simple de seguir. Las funciones auxiliares FormPrev, FormNext y ShowValues se muestran a continuación:

function TWebModule1.FormPrev:string;
begin
  Result:= '<form method=GET name=formprev>'+
    '<input type=hidden name=value value='+FFirstValue+'>'+
    '<input type=hidden name=dir value=prev></form>';
end;
 
function TWebModule1.FormNext:string;
begin
  Result:= '<form method=GET name=formnext>'+
    '<input type=hidden name=value value='+FLastValue+'>'+
    '<input type=hidden name=dir value=next></form>';
end;
 
function TWebModule1.ShowValues: string;
begin
  Result:= '<br>First value: '+FFirstValue+
           '<br>Last value: '+FLastValue+'<br>';
end;

Cuando se genera la tabla HTML, los valores primero y último que se muestran son almacenados en variables privadas que son propagadas a la siguiente llamada vía campos ocultos en los forms FormPrev y FormNext. Este es el código para almacenar los valores:

procedure TWebModule1.TableProducerFormatCell(Sender: TObject; CellRow,
  CellColumn: Integer; var BgColor: THTMLBgColor; var Align: THTMLAlign;
  var VAlign: THTMLVAlign; var CustomAttrs, CellData: String);
begin
  if (CellColumn=0) and (CellRow>0) then //Assuming first column is order key
  begin
    if StrToInt(CellData)<StrToInt(FFirstValue) then FFirstValue:= CellData;
    if StrToInt(CellData)>StrToInt(FLastValue) then FLastValue:= CellData;
  end;
end;

Note que este código asume que la primera columna de datos es la columna por la cual se ordena, y que es de tipo numérico entero. Las variables FFirstValue y FLastValue son strings que se inicializan en el evento DatasetTableProducer1.OnCreateContent:

procedure TWebModule1.TableProducerCreateContent(Sender: TObject;
  var Continue: Boolean);
begin
  cds.Close;
  with Query1 do
  begin
    Close;
    SQL.Text:= 'SELECT * FROM BIOLIFE';
    if parameter('dir')='prev' then
    begin
      SQL.Add('WHERE BIOLIFE."Species No"<'+Parameter('value'));
      SQL.Add('ORDER BY BIOLIFE."Species No" desc');
      cds.IndexName:= 'ixInverted';
      cds.Open;
      FNext:= True;
      FPrev:= cds.RecordCount>RecsPerPage;
      if FPrev then cds.Next//show last RecsPerPage records
      //(they are inverted from query's result due to index)
    end
    else
    begin
      if parameter('dir')='next' then
      begin
        SQL.Add('WHERE BIOLIFE."Species No">'+Parameter('value'));
        FPrev:= True;
      end else //first request
        FPrev:= False;
      SQL.Add('ORDER BY BIOLIFE."Species No" asc');
      cds.IndexName:= '';
      cds.Open;
      FNext:= cds.RecordCount>RecsPerPage;
    end;
  end; //with
  FFirstValue:= '9999999';
  FLastValue:= '0';
end;

En este evento se genera el SQL que se enviará al servidor de Bases de Datos, usando los parámetros propagados desde la última página ('value' y 'dir'). Las variables internas FFirstValue y FLastValue toman sus valores por defecto, y las banderas booleanas FNext y FPrev indican si hay o no más contenido hacia adelante o hacia atrás, respectivamente.

El movimiento hacia adelante es simple, sólo se considera un caso especial: cuando el parámetro 'dir' no tiene valor, significa que estamos en la primera página y por lo tanto no se necesita la condición WHERE.

El movimiento hacia atrás es un poco más complicado, porque necesitamos obtener los registros en orden inverso, y dar vuelta el resultado para mostrar los registros normalmente. Será más fácil explicarlo con un ejemplo:

Supongamos que tenemos los siguientes valores en la tabla:

1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11

y RecsPerPage se inicializa a 5. Entonces, para la primera página se obtienen los siguientes registros

1, 2, 3, 4, 5, 6

FNext se vuelve TRUE, FPrev será FALSE y se muestran los primeros 5 registros (por lo tanto FFirstValue = 1 y FLastValue = 5). El registro extra al final se usa para saber rápidamente si hay más registros adelante o atrás.

A continuación, si seguimos el enlace a la página siguiente obtenemos

6, 7, 8, 9, 10, 11

FNext = True, FPrev = True. Presionando nuevamente en 'Siguiente' obtenemos

11

Ahora FNext = False y FPrev = True. El enlace 'Siguiente' se reemplaza por un texto indicando que estamos al final de la tabla. Hasta ahora, todo bien. Presionamos en el enlace a la página anterior, y obtenemos los registros

10, 9, 8, 7, 6, 5

note el orden inverso, producto del indicador 'desc' en el ORDER BY. Para mostrar estos registros, activamos el índice ixInverted en el ClientDataset, y entonces tendremos

5, 6, 7, 8, 9, 10

¡Correcto! Tenemos los registros en el orden que corresponde, pero si mostramos los primeros 5, veremos

5, 6, 7, 8, 9

hemos perdido el registro 10. Para impedirlo, el código verifica si estamos recibiendo más de RecsPerPage registros y si es el caso, saltea el primero para obtener

6, 7, 8, 9, 10

Ahora si estamos bien.

Si me han seguido, pregúntense ¿que pasaría si ponemos RecsPerPage a un valor mayor que la cantidad de registros de la tabla completa? Traten de responder sin probarlo.

Y esto es todo, amigos! Espero haber sido claro. Por cualquier consulta pueden contactarme en ecullen@ciudad.com.ar

El código fuente de este artículo está disponible para descarga.
 


Copyright © 2002 Ernesto Cullen.

Se permite la publicación de este material por cualquier medio por parte de cualquiera siempre que este no sea modificado en contenido y se cite la fuente original.

Boycott Trend Micro!