Free Web Hosting by Netfirms
Web Hosting by Netfirms | Free Domain Names by Netfirms

Paginación en WebBroker usando ClientDatasets

Copyright © 2002 Ing. Ernesto Cullen

Este artículo fue publicado en el Boletín Pascal de Latium Software


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 consulta1. 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 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 via 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
 


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.