|
Paginación en WebBroker usando ClientDatasets
Copyright © 2002 Ing. Ernesto Cullen
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>
<#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 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.
|