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>
<#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> <#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();"><< Previous</a>'
else
ReplaceText:= 'First record shown'
else
if SameText('PageUp',TagString) then
if FNext then
ReplaceText:= '<a href="javascript:formnext.submit();">Next >></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: