Objetos de Sistema
El modelo de objetos que se emplea en la maquinaria de script XOne permite acceder a toda la funcionalidad de la aplicación con solo algunos objetos de entidad global y sin tener que memorizar una jerarquía de objetos y funciones propios del runtime, ya que se ha priorizado el conocimiento de la estructura de la aplicación (colecciones y relaciones entre ellas) que es lo más fácil para el desarrollador.
A continuación se enumeran los objetos que componen el modelo usado por la maquinaria script de XOne.
Elemento | Descripción |
---|---|
Modelo de Objetos en VBScript. | Modelo de Objetos en VBScript. |
Modelo de Objetos en JavaScript. | Modelo de Objetos en JavaScript . |
Ámbitos y Visibilidad
Una de las cosas más importantes a comprender antes de usar el modelo de objeto es comprender dentro de qué ámbito se ejecuta cada script, para tener acceso a los datos de manera correcta. En la maquinaria XOne se pueden encontrar los ámbitos de ejecución diferentes:
Objeto
Un script se está ejecutando en ámbito de objeto cuando la acción que lo ha invocado ha sido llamada desde un objeto.
Por ejemplo, las acciones de tipo <action> que se definen dentro de los nodos de comportamiento de una colección, como puede ser el caso de <create> están en el ámbito del objeto que las contiene.
Así cuando un objeto se crea nuevo, se ejecutan las acciones dentro del nodo <create> de dicho objeto y si alguna de ellas llama a un script, este se ejecutará en el ámbito del objeto recién creado.
En el ámbito de objeto, la variable global This contiene una referencia al objeto que está ejecutando el script, en el caso que nos ocupa, This sería el objeto que se está creando.
En este ámbito la variable global ThisDataColl es Nothing.
Las variables que se definan en este ámbito durarán mientras dure la ejecución del script y se destruirán al terminar la acción en cuestión.
Colección
Este ámbito es bastante menos común que el de objeto. Los scripts que se ejecutan en este ámbito son los que están definidos en nodos <coll-action>.
Cuando se llama a una acción de colección, el ámbito será el de la colección que contiene la acción que se está ejecutando.
El caso más representativo de este ámbito son las acciones <onlogon> que se ejecutan cuando se conecta el usuario a la aplicación. En este caso concreto, el ámbito en que se ejecuta cada script es el de la colección que contiene el nodo <onlogon>.
Cuando se ejecutan scripts en ámbito de colección, la variable global ThisDataColl contiene la colección que ha llamado el script y la variable This es Nothing.
Las variables definidas en este ámbito tendrán el mismo tiempo de vida que las del ámbito de objeto.
Ámbito local
Cuando se llama a una función (definida dentro de un archivo de inclusión, o dentro del mismo bloque de script) se crea un ámbito local en el que se pueden definir variables que siguen las mismas reglas que cualquier lenguaje que permita variables locales.
Las variables locales serán destruidas al salir de la función.
Visibilidad
Para cada objeto del modelo se cumplen las siguientes reglas de visibilidad:
- AppData es visible siempre desde cualquier ámbito y en cualquier circunstancia.
- This es visible mientras dure la ejecución del script. Las llamadas a funciones no salen del ámbito de ejecución. Esta misma regla es válida para ThisDataColl. Ambos objetos, en caso de estar definidos son visibles dentro de todas las funciones que se llamen durante la ejecución del script.
- User tiene la misma visibilidad que AppData y su existencia está definida solamente por la presencia de usuario conectado.
- Las variables definidas en el bloque principal de script (ámbito de objeto o colección) serán visibles para todas las funciones que se llamen dentro del script.
- No se puede declarar (DIM) una variable local con el mismo nombre que una variable global ya existente. Si se hace referencia a una variable definida en el ámbito global dentro de una función, se accederá a la variable global.
- Las variables locales no son visibles dentro de las funciones llamadas dentro de otra función (i.e. Si A declara la variable V1 y después llama a B, dentro de B no se tiene acceso a V1).
Anidamiento
Cuando un script ejecuta una acción que provoca la ejecución de eventos de la maquinaria:
(i.e. Cuando se ejecuta una asignación a una propiedad de un objeto) dicha acción puede llamar a su vez a otro script.\\
Es importante tener en cuenta que al llamase un script anidado dentro de otro que está ya en ejecución el valor de las variables globales puede cambiar, ya que la acción puede estar ejecutándose desde otro lugar diferente.
Se debe considerar cada acción anidada como una ejecución totalmente independiente. Las variables globales definidas dentro de un script no son visibles dentro de una acción anidada.
Si una acción quiere intercambiar datos con otra anidada dentro de ella tiene que usar métodos que no consistan en el uso de variables de script, sino que tienen que recurrir a almacenar dichos datos en algún lugar que sea visible para cualquier entorno de ejecución:
(i.e. Objetos de datos dentro de las colecciones, objeto AppData, stack de parámetros, etc.)
El anidamiento de scripts sigue las mismas reglas que el anidamiento de acciones XML de la maquinaria XOne, así que no nos extenderemos mucho en este concepto.
Persistencia
Aunque este concepto está relacionado con el ámbito de los datos, lo hemos separado por la importancia que tiene comprenderlo. Una vez asimilado el funcionamiento (tiempo de vida de los datos) de las variables locales y globales dentro de un script, nos referiremos al almacenamiento de datos en otros medios que aunque estén al alcance del código de script, no se trata de parte del runtime script en concreto, sino de la maquinaria XOne en general.
Así tenemos los siguientes casos:
- AppData contiene una lista de colecciones llamadas “globales”. Estas colecciones son las que están definidas en el mappings y para cada una de las definiciones <coll> habrá una y solo una colección global. Los objetos que contienen estas colecciones son visibles para todos los scripts que se ejecuten en cualquier momento, por lo que una acción puede usar una colección global para almacenar datos en un objeto y pasarle valores a otro script diferente.
Es importante tener en cuenta que si dentro de un script se cargan datos en una colección global y desde otro se limpia esta colección, los efectos tanto de una acción como de la otra tienen alcance dentro de ambos scripts (i.e. Si un objeto ejecuta un script en su acción <create> y dentro de ese script se asigna valor a una propiedad que llama a otro script en la acción <onchange>, el segundo script verá el mismo dato que ha asignado el primero, ya que se está operando sobre un objeto de la maquinaria. Si el script del <create> ha creado una variable global para guardar algún valor en él, esta variable no será visible dentro del script del <onchange>.
- Si dentro de un script se crea un objeto o una colección (i.e. Se llama a CreateClone de una colección global) y en este objeto o colección se cargan valores y se almacenan datos, dichos datos no serán visibles para ningún otro script anidado o no dentro del que ha creado el objeto o colección, ya que se trata de un objeto restringido al ámbito del script que lo ha creado.
- AppData define un stack de parámetros que se puede usar para pasar valores entre scripts que se ejecutan en ámbitos diferentes o que se anidan. Este stack tiene alcance global para toda la aplicación y se accede a él mediante AppData.PushValue y AppData.PopValue. Si dentro de este stack se introduce un obejto o colección creado dentro de un script, éste estará disponible para cualquier otro script que lo saque de allí.
- El objeto global User puede usarse como región de intercambio de datos. Si un script almacena datos en las propiedades de User, o en las variables de User, estos datos estarán disponibles para cualquier otro script, tanto anidado como ejecutado posteriormente, mientras no se modifiquen.
- Cualquier dato almacenado en base de datos puede recuperarse desde cualquier otro ámbito (esto puede parecer obvio, pero no vean las preguntas que he tenido que contestar en estos años).
Buenas prácticas e información útil
Antes de iniciarnos en el desarrollo de procesos mediante script es interesante revisar esta sección, ya que las cuestiones aqui planteadas son consultas comunes que se plantean con reiterada frecuencia. De ese modo, el objetivo es aclarar algunos conceptos claves que son elementales para la correcta comprensión y ejecución de los desarrollos.
Colecciones globales
Si una colección global se “llena” usando el método loadall, independientemente del lenguaje script empleado, estará llena después que se salga del script y por tanto el framework la verá así, llena.
Si una colección global se vacía o se filtra dentro de un script, estos cambios afectarán la ejecución del framework y por supuesto la ejecución de otros scripts.
Este efecto secundario puede explotarse para buscar determinadas funcionalidades, pero puede causar muchos dolores de cabeza si no se tiene en cuenta.
Por ejemplo, si la aplicación usa la colección “Clientes” para mostrar la lista de clientes en pantalla y desde un script se hace se aplica in filtro del siguiente modo:
AppData.GetCollection(“Clientes”).LinkFilter=”CODIGO=1”
Vamos a trabajar solo con este cliente, pero al salir del script, la colección global de clientes estará filtrada por ese código y, por tanto, en la lista de clientes de la aplicación solo saldrá el cliente 1.
Esto que puede parecer obvio es un problema bastante difícil de encontrar cuando se trata de aplicaciones complejas, por lo que es bueno tener en cuenta estas reglas para minimizar este problema:
- Siempre que sea posible, NO trabajar con las mismas colecciones en el framework (UI de la aplicación) y los scripts. No importa que se tengan dos colecciones exactamente iguales en el mappigs, los dolores de cabeza que se quita uno de arriba valen la doble administración.
- Si no se desea tener duplicadas las colecciones para UI y script, es bueno acostumbrarse a trabajar con una copia de la colección, así en vez de:
Set c = AppData.GetCollection(“Clientes”) c.LinkFilter = “CODIGO=1” c.StartBrowse
Lo más aconsejable es hacer:
Set c = AppData.GetCollection(“Clientes”).CreateClone ... resto del proceso ... ' No olvidar destruir la referencia Set c = Nothing
Una vez que se sale del ámbito en cuestión, la variable “c” se destruye, y como antes se había anulado la referencia, la colección local se destruye y la memoria se recupera.
La colección global “Clientes” no se ha tocado para nada, así que no afecta el comportamiento del UI de la aplicación.
Startbrowse y Loadall son conceptos independientes
Estas dos formas de recorrer una colección son totalmente diferentes y no interfieren para nada entre ellas, por lo que es importante comprender que es y como funciona cada una.
Aunque la explicación detallada está en la referencia, aquí nos limitamos a indicar algunos comportamientos incorrectos o no del todo adecuados que hemos encontrado en aplicaciones reales:
- Hacer LoadAll antes de recorrer una colección con StartBrowse.
Esto no solamente es innecesario, sino que puede ser contraproducente. LoadAll carga todos los objetos de la colección en memoria, por lo que puede ser una penalización grande a la ejecución de la aplicación.
De hecho el mecanismo de StartBrowse se ha desarrollado para evitar que haya que cargar todos los objetos de una colección en memoria para trabajar con ellos.
Si se va a recorrer una colección que contiene muchos objetos para hacer algún tipo de trabajo con ellos, NO llame a LoadAll. \\De hecho trate de NO llamar a LoadAll a menos que se trate de casos en los que se tenga la situación completamente controlada.
- Hacer LoadAll para contar la cantidad de elementos de una colección.
Esto no solo es una paletada como una catedral, sino que es innecesario.
Si se quiere contar la cantidad de elementos de una colección en la mayoría de las plataformas basta con StartBrowse(True) que cuenta los elementos. Si la plataforma no soporta el conteo por esta vía, lo más recomendable es preparar una pequeña colección en la cual se tenga un SQL de agrupación al que se le ponga un filtro y lo haga StartBrowse para leer el valor del conteo.
Cargar una cantidad indeterminada de objetos en memoria solamente para saber cuántos hay es un gasto innecesario de recursos y además consume mucho más tiempo que las soluciones anteriores.
- Modificar el CurrentItem. Esta práctica se soporta en algunas plataformas, pero está limitada por las características de la base de datos.
Aquellas bases de datos que permiten modificar una tabla con cursores abiertos no darán problemas, las que no lo permitan causarán errores de ejecución.
Como se trata de un error que no está asociado a un código sino a la base de datos, puede resultar difícil de determinar el problema que lo origina.
Se ha tratado que esto afecte lo menos posible pero siempre existen limitaciones. Si la cantidad de objetos a modificar no es grande, se pueden cargar todos con LoadAll y modificarlos allí.
Si se trata de una cantidad grande de objetos, se puede pensar en modificarlos con una sentencia SQL, o en su defecto almacenar localmente los Ids de los objetos afectados y posteriormente cargarlos y modificarlos.
Mantener referencias a objetos destruídos
Si en un script se crea una copia de una colección, o se está trabajando con una copia de una colección ya existente (como puede ser un contents, por ejemplo) cualquier referencia que se haga a un objeto de dicha colección se debe anular antes de salir del script.
Si la referencia a la colección se anula antes de salir del script, las referencias a los objetos que contiene dicha colección se deben anular ANTES de anular la colección.
Esto se debe a que una vez que se anula la referencia a la colección, la maquinaria de script tratará de destruirla y esta a su vez destruirá a los objetos que contiene. Si después se hace referencia al objeto, este tratará de trabajar con una colección que se ha destruido con el consecuente problema de acceso a memoria liberada.
En algunos lugares esto puede no dar problemas, en otros puede causar la salida inmediata del programa.
Por tanto, la idea es:
- Anular las referencias de forma inversa a su creación (Colección y después objeto, por tanto se anula objeto y después colección)
- No almacenar colecciones creadas localmente en instancias globales (una colección creada dentro de una función no debe almacenarse en una variable global, una colección creada dentro de un script no debe almacenarse en una variable de AppData ni en el stack de parámetros)
Ojo con la reentrada: bucles infinitos
Cuando un script puede ser llamado desde varias situaciones diferentes tiene que estar preparado para ser reentrante.
Un ejemplo es un script que se ejecuta dentro de un nodo <insert> y que a su vez llama a Save, o un script que se ejecuta dentro de un <onchange> que puede provocar la llamada a sí mismo al modificar la misma propiedad que lo ha ejecutado. Estos casos pueden provocar bucles infinitos que derivarán en un stack overflow.
En cualquiera de estos casos se deben usar marcas almacenadas en entornos visibles para cualquier script, como puede ser una propiedad del propio objeto, o las variables de la colección o el objeto en cuestión.
Así un script reentrante podría ser más o menos como este:
<insert.... <action... If This(“MAP_SAVING”) = 0 Then This(“MAP_SAVING”) = 1 . . . hacer cositas . . . This.Save ' Esto llama a esta acción otra vez This(“MAP_SAVING”) = 0 End If </action> </insert>
Cuando la llamada a Save provoque la ejecución de la acción anidada dentro del script que estamos ejecutando, se interrumpirá la ejecución en This.Save y se creará un nuevo ámbito del mismo objeto llamando nuevamente al código del script.
Lo primero que se hará es comprobar si MAP_SAVING tiene valor cero. Como no se cumple la condición, el script termina y sale, por lo que se recupera el ámbito anterior y el script continúa después de This.Save, se vuelve a limpiar la bandera y se termina la ejecución normal del script.