Agradecimientos
Este artículo, al igual que los programas que he escrito en 4D, se basan en una tecnología llamada GenRoTools.
Sin la ayuda de Bags, la implementación de soluciones via Servicios Web pueden ser realmente complicadas, por lo que quiero agradecer a Jeff Edwards y a Giovanni Porcari por haber producido esta herramienta de gran calidad. La licencia es gratuita, tanto a nivel de dearrollo como de instalación. La página de descarga se hará pública proximamente, pero mientras tanto, Jeff y Giovanni me han permitido distribuir GenRoTools junto con los ejemplos incluídos en este artículo.
Prerequisitos
Para poderle sacar partido al artículo, el lector debería leer mi artículo anterior.
Primer ejemplo: Servicio Web que facilita una lista de estados en E.E.U.U. en los que se encuentran los doctores
La intención de este ejemplo es la de crear un Servicio Web que devuelva un Bag con los estados americanos codificados de la siguiente manera:
El método del servidor que genera esta lista es “WS_ListaDeEstados”:
`Declaración del tipo de variable que devolveremos al cliente
SOAP DECLARATION(SOAPBLOB;Is BLOB ;SOAP Output ;"SOAPBLOB")
`Variable temporal utilizada para recoger ls lista de estados
ARRAY STRING(2;EstadosDeEEUU;0)
DEFAULT TABLE([Doctores])
ALL RECORDS
DISTINCT VALUES([Doctores]Estado;EstadosDeEEUU)
C_LONGINT($i;$Records)
$Records:=Size of array(EstadosDeEEUU)
For ($i;1;$Records)
GNT_Bags ("SetItem";->SOAPBLOB;"Estado."+EstadosDeEEUU{$i};->EstadosDeEEUU{$i})
End for
La línea de código más importante es:
GNT_Bags ("SetItem";->SOAPBLOB;"Estado."+EstadosDeEEUU{$i};
->EstadosDeEEUU{$i})
Añadimos un elemento al Bag mediante el primer parámetro, “SetItem”.
Nota: GenRoTools reemplazará la etiqueta y su valor correspondiente por la especificada mediante “SetItem”. Dicho de otra manera, si se desea añadir al Bag todos lo valores seleccionados, habrá que asegurarse de que la etiqueta es única.
El método del cliente que obtiene el Bag con los Estados es “Probar_ListaDeEstados”:
C_BLOB(vDebugBag)
vDebugBag:=WS_ListaDeEstados
C_STRING(75;vEtiquetaDeLista)
vEtiquetaDeLista:="Lista de Estados en E.E.U.U. disponibles:"
If (BLOB size(vDebugBag)#0)
C_LONGINT($width;$height)
GET FORM PROPERTIES([Table 1];"BagDebugList";$width;$height)
OpenCenteredWindow ($width;$height;Plain window ;"Estados")
DIALOG([Table 1];"BagDebugList")
CLOSE WINDOW
Else
ALERT("No se han podido obtener los Estados del servidor.")
End if
- El método WS_ListaDeEstados se ha creado automaticamente mediante el “Asistente Servicios Web” de 4D.
- El formulario “BagDebugList” contiene los siguientes elementos:

En el método de objeto “hBagDebugList” pondremos el siguiente método:
Case of
: (Form event=On Load )
C_LONGINT(hBagDebugList)
GNT_Bags ("BuildList";->vDebugBag;"hBagDebugList")
If (Is a list(hBagDebugList))
REDRAW LIST(hBagDebugList)
End if
: (Form event=On Clicked )
C_LONGINT($vlItemPos;$vlItemRef;$hSublist)
C_STRING(255;$vsItemText)
C_BOOLEAN($vbExpanded)
$vlItemPos:=Selected list item(hBagDebugList)
If ($vlItemPos>0)
C_STRING(2;vEstado)
GET LIST ITEM(hBagDebugList;$vlItemPos;$vlItemRef;$vsItemText;$hSublist;
$vbExpanded)
If (Is a list($hSublist))
DISABLE BUTTON(vOK)
Else
vEstado:=$vsItemText
ENABLE BUTTON(vOK)
End if
End if
: (Form event=On Unload )
If (Is a list(hBagDebugList))
CLEAR LIST(hBagDebugList;*)
End if
End case
GenRoTools nos ofrece un método muy útil que convierte un Bag en una lista jerárquica:
GNT_Bags ("BuildList";->vDebugBag;"hBagDebugList")
Si todo sale correctamente, veremos el resultado siguiente:

Segundo ejemplo: Servicio Web que facilita una lista de doctores localizados en una zona
Basándonos en el primer ejemplo, solicitaremos los doctores pertenecientes a un Estado. La ventaja de utilizar la lista generada por el servidor en el primer ejemplo es que el usuario no ha de adivinar qué Estados están disponibles. Esto facilita el trabajo al usuario y mejora su forma de trabajar. El método es el siguiente:
C_BLOB(vDebugBag)
vDebugBag:=WS_ListaDeEstados
C_STRING(75;vEtiquetaDeLista)
vEtiquetaDeLista:="Lista de Estados en E.E.U.U. disponibles:"
If (BLOB size(vDebugBag)#0)
C_LONGINT($width;$height)
GET FORM PROPERTIES([Table 1];"BagDebugList";$width;$height)
OpenCenteredWindow ($width;$height;Plain window ;"Estados")
DIALOG([Table 1];"BagDebugList")
CLOSE WINDOW
If (OK=1)
vDebugBag:=WS_ObtenerDoctores (vEstado)
C_STRING(75;vEtiquetaDeLista)
vEtiquetaDeLista:="Doctores en el estado de: "+vEstado
If (BLOB size(vDebugBag)#0)
C_LONGINT($width;$height)
GET FORM PROPERTIES([Table 1];"BagDebugList";$width;$height)
OpenCenteredWindow ($width;$height;Plain window ;"Doctores")
DIALOG([Table 1];"BagDebugList")
CLOSE WINDOW
Else
ALERT("No se han podido obtener los doctores del estado “"+vEstado+"”.")
End if
End if
Else
ALERT("No se han podido obtener los Estados del servidor.")
End if
- En primera instancia, obtenemos la lista de Estados disponibles. El método del objeto BagDebugList detecta el click en el valor (no la etiqueta), lo cual nos permite guardar el valor en la variable vEstado.
- Si cerramos la ventana mediante un click en OK, procedemos a obtener los doctores, pasando el valor de vEstado como parámetro. Al igual que en ejemplo 1, utilizamos el Asistente Servicios Web para generar el método WS_ObtenerDoctores. Éste llama al método con el mismo nombre en el servidor, que contiene lo siguiente:
COMPILER_WEB
SOAP DECLARATION(SOAPString5;Is String Var ;SOAP Input ;"SOAPString5")
SOAP DECLARATION(SOAPBLOB;Is BLOB ;SOAP Output ;"SOAPBLOB")
DEFAULT TABLE([Doctores])
QUERY([Doctores]Estado=SOAPString5)
C_LONGINT($i;$Records)
$Records:=Records in selection
If ($Records>0)
FIRST RECORD
For ($i;1;$Records)
GNT_Bags ("SetItem";->SOAPBLOB;"Doctores."+String([Doctores]ID)+".ID";
->[Doctores]ID)
GNT_Bags ("SetItem";->SOAPBLOB;"Doctores."+String([Doctores]ID)+".Nombre";
->[Doctores]Nombre)
GNT_Bags ("SetItem";->SOAPBLOB;"Doctores."+String([Doctores]ID)+".Apellido";
->[Doctores]Apellido)
GNT_Bags "SetItem";->SOAPBLOB;"Doctores."+String([Doctores]ID)
+".Especialidad";->[Doctores]Especialidad)
GNT_Bags ("SetItem";->SOAPBLOB;"Doctores."+String([Doctores]ID)+".Direccion";
->[Doctores]Direccion)
GNT_Bags ("SetItem";->SOAPBLOB;"Doctores."+String([Doctores]ID)+".Ciudad";
->[Doctores]Ciudad)
GNT_Bags ("SetItem";->SOAPBLOB;"Doctores."+String([Doctores]ID)+".Estado";
->[Doctores]Estado)
NEXT RECORD
End for
End if
- El método COMPILER_WEB contiene las siguientes declaraciones:
ARRAY LONGINT(aSOAPLongint;0)
ARRAY TEXT(aSOAPText;0)
C_LONGINT(SOAPLongint)
C_STRING(5;SOAPString5)
C_BLOB(SOAPBLOB)
- Seleccionamos los doctores pertenecientes al Estado pasado en la variable SOAPString5 (el valor de la variable vEstado enviada por el cliente) y añadimos los valores de cada cliente al Bag.
Nota: es importante destacar que la etiqueta es común por cada grupo de valores, mientras que la etiqueta de cada valor es única, lo que permite agruparlas en un mismo nodo. Por ejemplo:
Doctores. 5000063.ID --> 5000063
Doctores. 5000063.Nombre --> Kevin
Doctores. 5000063.Apellido --> Nguyen
Doctores. 5000063.Especialidad --> Podiatry
Doctores. 5000063.Dirección --> 28785 Via Pasa Tiempo
Doctores. 5000063.Ciudad --> Laguna Niguel
Doctores. 5000063.Estado --> CA
Resulta en la siguiente estructura jerárquica:
- En cada iteración, el código del doctor cambia, lo cual genera otro grupo de valores, y así sucesivamente.
Tercer ejemplo: Servicio Web que facilita una lista de doctores localizados en una zona (versión optimizada)
El método “WS_ObtenerDoctores” del servidor realiza su función, pero genera mucho tráfico ya que utiliza NEXT RECORD por cada iteración. Si hay pocos clientes no es un problema, pero si la selección es mucho mayor, la pérdida de velocidad puede ser importante. La versión mejorada contiene los siguientes cambios:
COMPILER_WEB
SOAP DECLARATION(SOAPString5;Is String Var ;SOAP Input ;"SOAPString5")
SOAP DECLARATION(SOAPBLOB;Is BLOB ;SOAP Output ;"SOAPBLOB")
DEFAULT TABLE([Doctores])
QUERY([Doctores]Estado=SOAPString5)
ARRAY LONGINT($aDoctorID;0)
ARRAY STRING(75;$aDoctorNombre;0)
ARRAY STRING(75;$aDoctorApellido;0)
ARRAY STRING(75;$aDoctorEspecialidad;0)
ARRAY STRING(75;$aDoctorDireccion;0)
ARRAY STRING(75;$aDoctorCiudad;0)
ARRAY STRING(5;$aDoctorEstado;0)
SELECTION TO ARRAY([Doctores]ID;$aDoctorID;[Doctores]Nombre;$aDoctorNombre;
[Doctores]Apellido;$aDoctorApellido;[Doctores]Especialidad;$aDoctorEspecialidad;
[Doctores]Direccion;$aDoctorDireccion;[Doctores]Ciudad;$aDoctorCiudad;
[Doctores]Estado;$aDoctorEstado)
C_LONGINT($i;$Records)
$Records:=Records in selection
For ($i;1;$Records)
GNT_Bags ("SetItem";->SOAPBLOB;"Doctores."+String($aDoctorID{$i})+".ID";
->$aDoctorID{$i})
GNT_Bags ("SetItem";->SOAPBLOB;"Doctores."+String($aDoctorID{$i})+".Nombre";
->$aDoctorNombre{$i})
GNT_Bags ("SetItem";->SOAPBLOB;"Doctores."+String($aDoctorID{$i})+".Apellido";
->$aDoctorApellido{$i})
GNT_Bags ("SetItem";->SOAPBLOB;"Doctores."+String($aDoctorID{$i})
+".Especialidad";->$aDoctorEspecialidad{$i})
GNT_Bags ("SetItem";->SOAPBLOB;"Doctores."+String($aDoctorID{$i})+".Direccion";
->$aDoctorDireccion{$i})
GNT_Bags ("SetItem";->SOAPBLOB;"Doctores."+String($aDoctorID{$i})+".Ciudad";
->$aDoctorCiudad{$i})
GNT_Bags ("SetItem";->SOAPBLOB;"Doctores."+String($aDoctorID{$i})+".Estado";
->$aDoctorEstado{$i})
End for
- La copia de valores a arrays está optimizado en 4D Server, lo cual permite generar un Bag mucho más rapidamente que mediante iteraciones via NEXT RECORD.
Cuarto ejemplo: Servicio Web que codifica información relacionada con un doctor
En este ejemplo recogeremos en un Bag toda la información relacionada con un doctor: datos personales y visitas realizadas. El método “Probar_InfoDeUnDoctor” en el cliente es el siguiente:
C_BLOB(vDebugBag)
vDebugBag:=WS_InfoDeUnDoctor (12226)
C_STRING(75;vEtiquetaDeLista)
vEtiquetaDeLista:="Información del doctor 12226:"
If (BLOB size(vDebugBag)#0)
C_LONGINT($width;$height)
GET FORM PROPERTIES([Table 1];"BagDebugList";$width;$height)
OpenCenteredWindow ($width;$height;Plain window ;"Doctor 12226")
DIALOG([Table 1];"BagDebugList")
CLOSE WINDOW
Else
ALERT("No se ha podido obtener información del doctor 12226.")
End if
- El código del doctor se pasa directamente como parámetro para simplificar el ejemplo. Idealmente, el usuario debería poder seleccionar un doctor de la lista y obtener toda la información. Dejo ese ejercicio para el lector.
El método “WS_InfoDeUnDoctor” del servidor contiene lo siguiente:
COMPILER_WEB
SOAP DECLARATION(SOAPLongint;Is LongInt ;SOAP Input ;"aSOAPLongint")
SOAP DECLARATION(SOAPBLOB;Is BLOB ;SOAP Output ;"SOAPBLOB")
DEFAULT TABLE([Doctores])
QUERY([Doctores]ID=SOAPLongint)
ARRAY LONGINT(aDoctorID;0)
ARRAY STRING(75;aDoctorNombre;0)
ARRAY STRING(75;aDoctorApellido;0)
ARRAY STRING(75;aDoctorEspecialidad;0)
ARRAY STRING(75;aDoctorDireccion;0)
ARRAY STRING(75;aDoctorCiudad;0)
ARRAY STRING(5;aDoctorEstado;0)
SELECTION TO ARRAY([Doctores]ID;aDoctorID;[Doctores]Nombre;aDoctorNombre;
[Doctores]Apellido;aDoctorApellido;[Doctores]Especialidad;aDoctorEspecialidad;
[Doctores]Direccion;aDoctorDireccion;[Doctores]Ciudad;aDoctorCiudad;
[Doctores]Estado;aDoctorEstado)
C_LONGINT($i;$Records)
$Records:=Records in selection
For ($i;1;$Records)
GNT_Bags ("SetItem";->SOAPBLOB;"Doctor."+String(aDoctorID{$i})+".ID";
->aDoctorID{$i})
GNT_Bags ("SetItem";->SOAPBLOB;"Doctor."+String(aDoctorID{$i})+".Nombre";
->aDoctorNombre{$i})
GNT_Bags ("SetItem";->SOAPBLOB;"Doctor."+String(aDoctorID{$i})+".Apellido";
->aDoctorApellido{$i})
GNT_Bags ("SetItem";->SOAPBLOB;"Doctor."+String(aDoctorID{$i})+".Especialidad";
->aDoctorEspecialidad{$i})
GNT_Bags ("SetItem";->SOAPBLOB;"Doctor."+String(aDoctorID{$i})+".Direccion";
->aDoctorDireccion{$i})
GNT_Bags ("SetItem";->SOAPBLOB;"Doctor."+String(aDoctorID{$i})+".Ciudad";
->aDoctorCiudad{$i})
GNT_Bags ("SetItem";->SOAPBLOB;"Doctor."+String(aDoctorID{$i})+".Estado";
->aDoctorEstado{$i})
ARRAY DATE(aVisitaFecha;0)
ARRAY LONGINT(aVisitaHora;0)
ARRAY TEXT(aVisitaProposito;0)
ARRAY BOOLEAN(aVisitaPagado;0)
DEFAULT TABLE([Visitas])
QUERY([Visitas]ID=aDoctorID{$i})
SELECTION TO ARRAY([Visitas]Fecha;aVisitaFecha;[Visitas]Hora;aVisitaHora;
[Visitas]Proposito;aVisitaProposito;[Visitas]Pagado;aVisitaPagado)
C_LONGINT($j;$Visitas)
$Visitas:=Records in selection
C_BLOB(BagVisitas)
For ($j;1;$Visitas)
C_TIME(vTime)
vTime:=aVisitaHora{$j}
C_STRING(25;vTimeString)
vTimeString:=String(vTime;HH MM SS )
GNT_Bags ("SetItem";->BagVisitas;String($j)+".Fecha";->aVisitaFecha{$j})
GNT_Bags ("SetItem";->BagVisitas;String($j)+".Hora";->vTimeString)
GNT_Bags ("SetItem";->BagVisitas;String($j)+".Proposito";->aVisitaProposito{$j})
GNT_Bags ("SetItem";->BagVisitas;String($j)+".Pagado";->aVisitaPagado{$j})
End for
GNT_Bags ("SetItem";->SOAPBLOB;"Doctor."+String(aDoctorID{$i})+".Visitas";
->BagVisitas)
End for
- Este método está basado en el tercer ejemplo.
- Cabe destacar la inclusión de las visitas en un Bag secundario (BagVisitas) que luego procedemos a añadir en el Bag que contiene el resto de la información perteneciente al doctor.
Si todo sale correctamente, veremos el resultado siguiente:

Obteniendo valores de un Bag
La utilidad de GenRoTools que nos permite convertir un Bag en una lista jerárquica es de fenomenal ayuda, ya que nos permite estudiar el Bag que recibimos del servidor. A la hora de depurar, esta opción nos puede ahorrar muchísimo tiempo.
Ahora bien, una vez tenemos los datos que queremos codificados en un Bag, ¿cómo podemos extraerlos? Mediante el selector “GetItem”:
GNT_Bags ("GetItem";Bag;Etiqueta;VariableDeDestino)
Bag -> Puntero al Bag
Etiqueta -> Referencia de la que queremos obtener el valor
VariableDeDestino <- Valor asociado a la etiqueta
Centrémonos en el Bag devuelto por el servidor en el cuarto ejemplo.
Para extraer el valor de “Nombre”:
C_TEXT(vNombre)
GNT_Bags ("GetItem";->vDebugBag;”Doctor.12226.Nombre”;->vNombre)
Para extraer el Bag “Visitas” que contiene las visitas:
C_BLOB(vVisitas)
GNT_Bags ("GetItem";->vDebugBag;”Doctor.12226.Visitas”;->vVisitas)
Para obtener el número de visitas:
C_TEXT(vNumeroItems)
GNT_Bags ("CountItems";->vDebugBag;”Doctor.12226. Visitas”;->vNumeroItems)
Para obtener el número de valores pertenecientes al doctor:
C_TEXT(vNumeroItems)
GNT_Bags ("CountItems";->vDebugBag;”Doctor.12226”;->vNumeroItems)
GenRoTools automaticamente convierte el valor guardado en el Bag al tipo de la variable de destino. Por ejemplo, el valor de la etiqueta “ID” es Longint, pero podemos convertirlo a String muy facilmente mediante:
C_TEXT(vStringID)
GNT_Bags ("GetItem";->vDebugBag;”Doctor.12226.ID”;->vStringID)
Conclusión
La utilización de Bags simplifica enormemente la codificación de datos para ser traspasados mediante Servicios Web. Además, el código es de fácil lectura y la transmisión es óptima ya que se envía todo de una vez. La extracción de los valores es simple y limpia, ya que los datos están organizados jerarquicamente. GenRoTools provee de una herramienta muy útil a la hora de depurar: la conversión de un Bag en una lista jerárquica permite analizar el contenido del Bag sin tener que escribir rutinas de extracción.
Me gustaría comentar un pequeño detalle: la conversión de un Bag con muchos registros a una lista jerárquica es lenta. Durante el desarrollo esto no es quizá muy importante, ya que al fin y al cabo se trata de un elemento de depuración. Sin embargo, si se desea implementar esta opción en la versión final que el usuario manipulará, se recomienda compilar el programa.
Un último comentario. La documentación distribuida actualmente no es muy buena, pero con un poco de paciencia se puede salir adelante. En mi caso, decidí escribir unos métodos propios de apoyo para no tener que memorizar el order de parámetros. Por ejemplo, el método “GetBagWithLabelFromBag”:
C_POINTER($1;$DestinationBagPtr;$3;$SourceBagPtr)
C_TEXT($2;$LabelToExtract)
$DestinationBagPtr:=$1
$LabelToExtract:=$2
$SourceBagPtr:=$3
`Obtain the desired nested bag to the destination BLOB
GNT_Bags ("GetItem";$SourceBagPtr;$LabelToExtract;$DestinationBagPtr)
Este método permite obtener un Bag dentro de otro Bag. Lo utilizo de la siguiente manera:
C_BLOB(vVisitas)
GetBagWithLabelFromBag(->vVisitas;”Doctor.12226.Visitas”;->vDebugBag)
Una última nota acerca de la instalación: GenRoTools ha de instalarse mediante 4D Insider, pues se trata de un componente. Luego, en la estructura donde se desee utilizar ha de ponerse la siguiente línea en en método de la base de datos On Startup:
GNT_GenRoTools ("OnDatabaseStart")
Aunque he intentado cubrir las partes más significativas, es imposible cubrirlo todo. Espero que este artículo haya servido como ejemplo y permita al lector adentrarse en el genial mundo de los Bags. ¡Adelante y ánimo!
Tito Ciuro
Miami – Junio 2005
Artículo general
El suave perfume de los servicios Web Anexo 1
Servicios Web en práctica