Sie sind auf Seite 1von 35

https://foxydb.wordpress.

com/

En la siguiente tabla se muestra la secuencia de activación general de los eventos de Visual FoxPro. Se supone que la propiedad AutoOpenTables
del entorno de datos está establecida en el valor verdadero (.T.). Otros eventos pueden tener lugar en base a interacciones de usuario y a respuesta
del sistema.

Objeto Eventos
Entorno de datos BeforeOpenTables
Conjunto de formularios Load
Formulario Load
Cursor o cursores del entorno de Init
datos
Entorno de datos Init
Objetos 1 Init
Formulario Init
Conjunto de formularios Init
Conjunto de formularios Activate
Formulario Activate
Object1 2 When
Formulario GotFocus
Object1 GotFocus
Object1 Message
Object1 Valid 3
Object1 LostFocus
Object2 3 When
Object2 GotFocus
Object2 Message
Object2 Valid 4
Object2 LostFocus
Formulario QueryUnload
Formulario Destroy
Objeto 5 Destroy
Formulario Unload
Conjunto de formularios Unload
Entorno de datos AfterCloseTables
Entorno de datos Destroy
Cursor o cursores del entorno de Destroy
datos
1 Para cada objeto, desde el objeto más interno hasta el contenedor más externo
2 Primer objeto según el orden de tabulación
3 Siguiente objeto que va a recibir el enfoque
4 Cuando el objeto pierde el enfoque
5 Para cada objeto, desde el contenedor más externo hasta el objeto más interno

¿Cuándo ocurren los eventos?

Texto original: When Does It Happen?


http://www.jamesbooth.com/eventorder.htm
Autor: Jim Booth
Traducido por: Ana María Bisbé York

En el desarrollo de aplicaciones en Visual FoxPro somos siempre dependientes de la ocurrencia de


los eventos. Cada vez que tecleamos o damos clic del ratón es muy importante para el correcto
funcionamiento de la aplicación. En realidad ¿cuánto conoce acerca de cuándo ocurren las cosas en
VFP? Este mes vamos a examinar algunos de los eventos importantes y encontrar realmente en qué
momento ocurren.

Eventos y métodos.
Primero, antes de meternos en los eventos que se ejecutan durante varias operaciones en Visual
FoxPro, permítanme clasificar los eventos y métodos que se encuentran disponibles en el producto.
Prefiero referirme a estas localizaciones de código como métodos de evento y métodos. La
diferencia entre un método de evento y un simple método está en cómo se activa. Un método
simple solo se dispara si una línea de código lo ha llamado, puede tener un comportamiento
predeterminado dentro de VFP o puede simplemente estar donde se pueda escribir el código y
luego llamar a ese código cuando desee.
Un método de evento es automáticamente llamado si un evento ocurre, por ejemplo el método de
evento KeyPress de un control será automáticamente llamado en cualquier momento que el
usuario presione una tecla mientras el foco lo tenga este control. Es importante diferenciar entre el
evento (presionar la tecla) y el método de evento (KeyPress) que se activa como resultado del
evento que ocurre. Llamar a un método de evento en un código, no causa que el evento asociado
ocurra. Simplemente corre el código de ese método de evento.

Tomando y perdiendo el foco.


Existe un número de métodos de evento y métodos asociados con el recibir y/o perder foco del
control. Los métodos de evento relacionados con el recibimiento o pérdida del foco son When
(método de evento), GotFocus (método de evento), SetFocus (método), Valid (método de evento) y
LostFocus (método de evento).

Para utilizar las ventajas del modelo de eventos en Visual FoxPro es necesario entender claramente
los métodos de eventos que son activados y el punto en el que son activados. En el proceso de
recibir el foco la secuencia de métodos de eventos es 1 – When y luego 2 – GotFocus

El método de evento when puede ser pensado como el lugar para determinar si se le permitirá o no
al foco llegar al control. Devolviendo un valor .F. desde el método de evento When evita que el
foco llegue al control. Una vez que el evento devuelve un valor .T. el GotFocus es disparado (si el
When devuelve .F. no se ejecuta el GotFocus.) El GotFocus puede ser clasificado como un método
de evento en el cual se prepara al control para recibir el foco, ya que solo se dispara si el control en
efecto recibirá el foco.
El método SetFocus tiene un comportamiento predeterminado en el inicio del proceso de recibir el
foco por el control. El método SetFocus de un control es similar a la variable de foxpro2.x _curobj
Existen dos controles que actúan ligeramente diferente con relación al método de evento When.
Son el ListBox y Combobox. Con estos dos controles, el evento When se dispara de igual forma que
para otros controles, cuando llega el foco y antes de que el objeto en realidad lo tome. El When
puede devolver .F. y evita que el foco llegue al control.
Sin embargo, con listas y combos, el When también se dispara a cada momento cuando el item
seleccionado cambia. Debido a este comportamiento es importante ser cuidadosos con el código
que se coloca en el evento When de listas y combos. Debe estar seguro que el When se disparará
tantas veces para estos controles, como el usuario navegue por sus listas.
El proceso de pérdida del foco es similar. Existen dos métodos de evento involucrados, Valid y
LostFocus. El Valid se dispara primero y puede ser utilizado para evitar que el control pierda el foco.
Devolver .F. en el Valid ocasionará que el control retendrá el foco. Devolver .T. del valid permitirá al
foco abandonar el control y con seguridad se disparará el LostFocus. Igual que el When y GotFocus,
el Valid puede ser usado para decidir si se permitirá al foco abandonar el control y el LostFocus
puede ser utilizado para controlar las reacciones del control cuando pierda el foco.

Actualizando los datos.


Visual FoxPro es una herramienta de administración de Datos y como tal tiene mucho poder y es
muy fácil de manipular las capacidades de amarre. Muchos de los controles de VFP son capaces de
ser atados a fuentes de datos fijando sus valores en la propiedad ControlSource. El uso de la
propiedad ControlSource provoca que el control automáticamente actualice el origen de datos
(ControlSource) cuando el valor del control es cambiado.
Aunque, cualquier cosa en la vida, la actualización del ControlSource toma lugar en un punto de
tiempo específico. Conocer exactamente cuando ocurre la actualización es crítico para obtener la
mayor flexibilidad de los controles de VFP.
Existe un grupo de situaciones relacionadas con la actualización del ControlSource, a lo que algunos
llaman “bugs”. Uno de estos casos fue cuando un desarrollador que llamaba al thisform.refresh()
en el evento InteractiveChange de un cuadro de texto y se quejaba porque el cuadro de texto
mantenía el mismo valor del ControlSource. Otros han encontrado que usando el GetFldState(-1)
desde el botón de la barra de herramientas no refleja los cambios del control que tiene el foco,
cuando el botón de la barra de herramientas es pinchado. Ninguno de estos son bugs, ambos
ocurren “por diseño”. ¿Qué?!!

Si, el diseño de los controles en VFP es tal que ellos actualizan su ControlSource con su Value
inmediatamente antes de que sea llamado el evento Valid. Si el evento Valid no es llamado
entonces el ControlSource no será actualizado.
En la sección previa hemos discutido el evento Valid y encontramos que se activa en respuesta a la
intención del control de perder el foco.
En el primer caso, el InteractiveChange, el control no pierde el foco, no se dispara el Valid y el
ControlSource retiene su valor original, la llamada a refrescar (Refresh) del formulario causa que el
control también sea refrescado, durante su refresque re_ lee su ControlSource (el que todavía tiene
su control original)
En el segundo caso, la barra de herramientas, el problema se encuentra en el hecho de que las
barras de herramientas y menús nunca reciben el foco. Como el toolbar nunca recibe el foco el
control nunca pierde el foco y el Valid nunca será disparado y el ControlSource no será actualizado.
Antes de mirar las soluciones, vamos a correr un pequeño test para ver los efectos de este
comportamiento. En esta prueba cree un formulario con un cuadro de texto (textbox) que tenga un
campo de una tabla como ControlSource. Luego vaya a cada uno de los siguientes eventos y entre:
DEBUGOUT “<Event Name> ControlSource: “ + EVALUATE(THIS.ControlSource) ;
+ “Value: “ + THIS.Value

Reemplace <Event Name> con el nombre del método de evento en particular en el que está
colocando el código. Coloque este código en los métodos de evento: GotFocus, InteractiveChange,
KeyPress, Valid, y LostFocus.

Luego, abra el depurador y corra el formulario. Teclee el nuevo valor dentro del textbox y luego
presione Tab para salir del textbox. En la ventana del Depurador verá unos resultados similares a la
tabla que se muestra a continuación:
Método de
ControlSource Value Comentarios
Evento
El ControlSource y Value son iguales en el textbox que tiene el
GotFocus Jones Jones
control
Cuando escribe una letra, el primer método de evento que se
activa es el InteractiveChange. Durante el InteractiveChange la
InteractiveChange Jones S
propiedad Value fue alterada pero el ControlSource retiene su
valor original.
Después del InteractiveChange, el método de evento KeyPress
KeyPress Jones S es llamado. El ControlSource se mantiene inalterado con
relación al Value
InteractiveChange Jones Sm Este proceso continúa mientras continúe escribiendo en el
control textbox.
KeyPress Jones Sm
InteractiveChange Jones Smi
KeyPress Jones Smi
InteractiveChange Jones Smit
KeyPress Jones Smit
InteractiveChange Jones Smith
KeyPress Jones Smith
Esto es cuando se presiona el Tabulador (KeyPress) que se usa
KeyPress Jones Smith
para salir del control
Ahora, el método de evento Valid es activado, el ControlSource
Valid Smith Smith
es actualizado con el nuevo valor
Como el LostFocus se activa después del Valid, el ControlSource
LostFocus Smith Smith
ya tiene el nuevo valor.

Observe que en la tabla hay un período de tiempo en el que el valor del ControlSource no es igual
al Value del Control. Si se hubiera refrescado el control en ese tiempo el valor del control (Value)
hubiera sido revertido con el valor actual del origen de datos (ControlSource).
Si no presiona el tabulador después de introducir el dato y hace clic en la barra de herramientas o
selecciona una opción del menú. El textbox va a retener aun el foco. Esto significa que el valid no se
ha disparado y el ControlSource no será actualizado. El código ejecutado por el botón del toolbar (o
menú de opción) utilizado GetFldState(-1) para chequear los campos modificados, no verá que el
campo actual ha sido modificado. Esto es debido a que el campo no ha cambiado aún.
La solución es forzar el control actual a actualizar el ControlSource en cualquier código de la barra
de herramientas u opción de menú que utilice GetFldState() o TableUpdate(). Esto lo puede hacer
simplemente llamando el SetFocus de ese control. Llamando al SetFocus del control provoca que el
VFP ejecute primero el Valid y LostFocus antes de ejecutar el método SetFocus. Así el control va a
actualizar su ControlSource.
Debe tener mucha precaución obrando de esta manera. Existe siempre la posibilidad de que el
formulario actual no tenga control activo o que el control activo no tenga método SetFocus. Este
código de ejemplo muestra cómo puede verificar estas situaciones.
IF TYPE(“_screen.ActiveForm.ActiveControl.Name”) = “C”
IF PEMSTATUS(_screen.ActiveForm.ActiveControl,”SetFocus”,5)
_screen.ActiveForm.ActiveControl.SetFocus()
ENDIF
ENDIF
* El resto de su código va aquí.
El código de arriba verifica el tipo de la propiedad Name del control activo en el formulario activo.
Si la propiedad Name es de caracteres entonces podemos asumir con seguridad que el control
activo es un objeto. El segundo IF verifica si el ActiveControl tiene un método SetFocus. Si lo tiene,
lo llama. Llamar al SetFocus provoca el proceso de pérdida del foco llamando los métodos de
evento Valid y LostFocus y actualizando el ControlSource.
¿Por qué es esto tan poco intuitivo? ¿Por qué no trabaja esto de la forma en que yo pienso que
trabaja? ¿Por qué es tan raro? Bueno, su rareza es algo relativo, para alguna gente este
comportamiento pudiera ser raro, para otros es visto como la mejor manera de hacer que las cosas
funcionen. Para una persona es comida, lo que para otra es veneno.
Lo importante en este artículo es la prueba que se ha realizado. Los resultados son solo descriptivos
del modelo de evento para un conjunto particular de procesos en VFP. Ser capaz de crear una lista
de eventos y el estado de las cosas durante estos eventos es la utilidad real de este artículo.
Puede tratar de memorizar cada matiz de Visual FoxPro si desea. No creo que nunca llegue a
conocerlos a todos y tampoco creo que sea un buen uso del tiempo memorizar las peculiaridades
de cada herramienta. Lo que si es un buen uso del tiempo es dejar un proyecto de desarrollo de la
aplicación por unos minutos y construir una pequeña prueba para el estudio de que es lo que pasa
en una situación determinada. Después regresar al proyecto y programar en correspondencia con
lo que ha aprendido durante la prueba.
Lo que nunca te contó tu madre sobre instanciar y destruir formularios
Sesión original: What Your Mother Never Told You About Form Instantiation and
Destruction (DevEssentials 2004)
Autor: Drew Speedie
Traducido por: Ana María Bisbé York

Resumen
Esta sesión intenta ayudar a entender mejor la secuencia normal de eventos en VFP cuando los
formularios se instancian y se destruyen ... existe mucho más los eventos Init y Destroy. Dotado de
este conocimiento, puede depurar problemas e implementar buenas técnicas como las que vamos
a demostrar aquí.

Todos los ejemplos se pueden probar en VFP 8 o VFP 9, porque no se utiliza código específico para
VFP 9. La mayoría de los ejemplos se aplican a todas las versiones de VFP; pero algunos de los
ejemplos utilizan las funciones BINDEVENT()s y las características de la clase DataEnvironment que
fueron agregadas en VFP 8.

La mayoría de los ejemplos se pueden ejecutar desde la interfaz DEMO.APP, aunque algunos deben
comenzar con CLEAR ALL/CLOSE ALL, y deben ejecutarse desde la ventana de comandos.
DEMO.APP es el único archivo incluido con la presentación. Una vez que selecciona los botones Run
(Ejecutar) o Source (Fuente) desde la interfaz, los archivos con código fuente para ese ejemplo se
copian al disco, en la carpeta donde se encuentra DEMO.APP. A partir de ahí, puede ejecutar los
ejemplos desde la ventana de comandos. La mayoría pueden ser ejecutados directamente desde la
interfaz DEMO.APP.

Las bases LISAG (Load-Init-Show-Activate-GotFocus) y QRDU (QueryUnload- Release- Destroy-


Unload)
Para implementar exitosamente los escenarios de instanciación y destrucción de un formulario, lo
primero que debe entender es la secuencia nativa de los eventos.

Instanciación
Como se demuestra en LISAG_QRDU.SCX, aquí está la lista de eventos que ocurren durante la
instanciación:
1. Evento Form.DataEnvironment.OpenTables (Tablas/vistas en el Entorno de datos
DataEnvironment no están en uso USE() o abiertas)
2. Evento Form.DataEnvironment.BeforeOpenTables (Tablas/vistas en el Entorno de datos
DataEnvironment no están en uso USE() o abiertas)
3. Evento Form.Load (Tablas/vistas en el Entorno de datos DataEnvironment están en uso USE()
o abiertas)
4. Evento Init de cualquier objeto cursor en DataEnvironment
5. Evento Form.DataEnvironment.Init
6. Evento Init de cada miembro del formulario que es instanciado
7. Evento Form.Init
8. Evento Form.Show
9. Evento Form.Activate
10. Evento When del primer control del formulario en el orden de tabulación (tab order)
11. Evento Form.GotFocus
12. Evento GotFocus del primer control del formulario en el orden de tabulación

Destrucción
Como se demuestra en LISAG_QRDU.SCX, aquí está la lista de eventos que ocurren durante la
destrucción cuando el formulario es cerrado por el usuario haciendo Clic en la "X" en la esquina
superior derecha de la barra de título del formulario ... o ... por el usuario, seleccionando la opción
Cerrar desde el ControlBox en la esquina superior izquierda de la barra de título del formulario:

1. Evento Form.QueryUnload
2. Evento Form.Destroy
3. Evento Destroy para cada uno de los miembros del formulario.
4. Evento Form.Unload (Tablas/vistas en el Entorno de datos DataEnvironment están en uso
USE() o abiertas)
5. Evento Form.DataEnvironment.CloseTables (Tablas/vistas en el Entorno de datos
DataEnvironment no están en uso USE() o abiertas)
6. Evento Form.DataEnvironment.Destroy
7. Evento Destroy para cada cursor en el DataEnvironment

Como se demuestra en LISAG_QRDU.SCX, aquí está la lista de eventos que ocurren durante la
destrucción cuando el formulario es cerrado por una llamada a THISFORM.Release(), por ejemplo,
al hacer Clic en un botón Aceptar.

1. Evento Form.Release
2. Evento Form.Destroy
3. Evento Destroy para cada uno de los miembros del formulario.
4. Evento Form.Unload (Tablas/vistas en el Entorno de datos DataEnvironment están en uso
USE() o abiertas)
5. Evento Form.DataEnvironment.CloseTables (Tablas/vistas en el Entorno de datos
DataEnvironment no están en uso USE() o abiertas)
6. Evento Form.DataEnvironment.Destroy
7. Evento Destroy para cada cursor en el DataEnvironment

Instanciación / Destrucción de miembros contenedores


Como demostrará posteriormente el ejemplo LISAG_QRDU2.SCX, los contenedores se instancian
"de dentro hacia afuera" de igual forma que hace el propio formulario (debido a que también es un
contenedor) ... El Init de los miembros contenidos de disparan antes que el Init del contenedor
padre. En la destrucción ocurre lo contrario ... el Destroy del contenedor se dispara antes que el
Destroy de sus miembros contenidos.

Cuando el formulario tiene establecido valores para DEClass, DEClassLibrary


Como muestra el ejemplo LISAG_QRDU_DE.SCX, cuando un formulario establece valores para
DEClass, DEClassLibrary (disponibles a partir de VFP 8.0), las cosas son ligeramente diferentes.
Debido a que el objeto DataEnvironment es un objeto completamente separado, se instancia
completamente antes del Form.Load.

El Init de los miembros del DataEnvironment: Cursor, CursorAdapter, y Relation se disparan antes
que el Init del DataEnvironment, siguiendo el comportamiento nativo de VFP donde el Init de los
miembros ocurre antes que el Init del contenedor padre.
El evento Destroy del DataEnvironment ocurre antes que el Destroy de sus miembros.

Lo que nunca te contó tu madre


Los ejemplos LISAG_QRDU*.SCX demuestran algunos aspectos de interés, muchos de los cuales
pudiera no encontrar intuitivos:
Form
1. En dependencia de cómo se cierra el formulario, se ejecuta el método Release o el evento
QueryUnload; pero no ambos ... Form.Release no es un buen lugar para colocar código, no
importa lo que sea, que se deba ejecuta cada vez que se destruya el formulario. Utilice en su
lugar los eventos Destroy o Unload.
2. Debido a que muchos comandos como SET TALK están limitados a la sesión privada de datos
(ver DATASESSION TO en la ayuda de VFP para ver la lista de estos comandos SET), decidir en
que lugar va a asignar valores a estos comandos SET depende de cómo abre sus datos. Si no
utiliza nunca DataEnvironment es fácil: los comandos SET para la sesión privada de datos se
colocan en el Load de la clase form. Si utiliza DataEnvironment nativo en un formulario
basado en .SCX, tendrá que establecer estos comandos SET en
DataEnvironment.OpenTables/BeforeOpenTables, antes de que las tablas/cursores sean
abiertos. Si implementa un DataEnvironment de usuario especificado en las propiedades
DEClass/DEClassLibrary (VFP 8.0 y superior), podrá establecer los comandos SET para la
sesión privada de datos en el método Init de la clase DataEnvironment; pero sea consciente,
que el Init de los miembros Cursor, CursorAdapter, y los objetos Relation se disparan antes
que DataEnvironment.Init. Vea además un par de secciones a continuación en este
documento.

3. Al destruir un formulario no se disparan los eventos Form.Deactivate ni Form.LostFocus

DataEnvironment

1. El DataEnvironment abre sus datos implícitamente entre el


DataEnvironment.BeforeOpenTables y el Form.Load() ... inmediatamente antes del
Form.Load.

2. El DataEnvironment NO abre los datos en el evento OpenTable -- OpenTables es de uso


opcional para abrir programaticamente los datos si se utiliza AutoOpenTables = .F.

3. El evento BeforeOpenTables no se dispara antes (Before) que el evento OpenTables,


BeforeOpenTables está mal nombrado y se debió llamar algo como
AfterOpenTablesBeforeImplicitOpenTables.
(DespuesAbrirTablasAntesImplicitamenteAbrirTablas

4. El DataEnvironment.Cursor.Init se dispara DESPUÉS que el cursor ya fue abierto y DESPUÉS


del Form.Load

5. El DataEnvironment.Init se dispara DESPUÉS que el DataEnvironment halla cumplido todas


las misiones encomendadas. DESPUÉS que los cursores han sido puestos en uso, y DESPUÉS
del Form.Load. Excepto cuando el DataEnvironment es una instancia de usuario especificada
en las propiedades DEClass/DEClassLibrary, en ese caso se instancia completamente antes
de Form.Load.

6. Consecuente con el comportamiento de OpenTables y cuando las tablas se abren


explícitamente por el DataEnvironment, el mismo cierra las tablas ANTES del método
CloseTables.
7. Mientras la secuencia de instanciación / destrucción para el formulario y sus miembros es
consistente, la secuencia de instanciación / destrucción para el DataEnvironment depende
de su implementación: DataEnvironment nativo para un formulario basado en .SCX, DEClass
DataEnvironment de usuario para un formulario basado en .SCX o .VCX.

Llamar THISFORM.Metodos o consultar THISFORM.Propiedades desde el DataEnvironment


Los ejemplos LISAG_DE*.SCX demuestran comportamiento inconsistente relacionado con llamar a
métodos del formulario (THISFORM) desde el DataEnvironment.

Lo que nunca te dijo tu madre

DataEnvironment nativo de VFP para un formulario (.SCX)


Cuando un formulario basado en .SCX utiliza DataEnvironment nativo de VFP, el DataEnvironment
se instancia primero que el formulario como tal. Ver los ejemplos LISAG_QRDU*.SCX, los que
demuestran el su comportamiento nativo. Sin embargo…
1. LISAG_DE.SCX demuestra que las llamadas a métodos de formulario (nativos o de usuario),
desde métodos del DataEnvironment que se disparan antes del Form.Load son
¡COMPLETAMENTE IGNORADAS!. Por ejemplo, las llamadas a THISFORM.Metodos desde los
eventos de DataEnvironment OpenTables y BeforeOpenTables no hacen nada, no se invoca
ningún método y no se genera un error. Supongo que VFP no ha iniciado todavía la
instanciación del formulario y por tanto, no reconoce THISFORM como un objeto; pero yo
debería esperar que se generara un error, como hace SYS(1271,THISFORM).

2. LISAG_DE2.SCX demuestra que las llamadas a métodos de formulario (nativos o de usuario),


desde métodos del DataEnvironment que se disparan antes del Form.Load SÍ disparan esos
métodos EN CASO que hayan sido definidos en la clase Form a partir de la cual hereda el
formulario actual. Por supuesto, todos los métodos nativos de VFP heredan de la clase base
Form de VFP. Entonces, el código que se ejecuta es el heredado de la clase Form, NO
cualquier código ubicado en los métodos nativos o de usuarios del formulario instanciado.
Una vez que se dispara el Form.Load, la llamada a estos mismos métodos disparan los
métodos para el nivel instanciado.

3. De igual forma, si en el DataEnvironment que se dispara antes del Form.Load, consulta el


valor de una propiedad (nativa o de usuario) que se establece explícitamente en la ficha
Propiedades a nivel del formulario instanciado, los valores son los que sean
predeterminados por VFP para esa propiedad (.F. para todas las propiedades de usuario),
como si hubiera establecido las propiedades como predeterminadas en la ficha propiedades.
Sin embargo; para las propiedades establecidas en la ventana propiedades de cualquier clase
padre del formulario instanciado, el valor es evaluado adecuadamente, tal y como
esperamos. Sin embargo, puede establecer una propiedad en cualquier método del
DataEnvironment.
4. En los métodos de DataEnvironment, IntelliSense no muestra NINGUN PEM (Propiedades,
Eventos, Métodos) de usuario, ya sea de la instancia actual o de alguna clase padre de la
instancia actual del formulario.

Conclusión

La conclusión es que se puede abstraer del comportamiento de DataEnvironment para métodos de


usuario de formulario, ya que estos métodos están basados en formularios .SCX. Y debe recordar
que el código que coloque en estos métodos en el nivel instanciado del .SCX será completamente
ignorado, cuando esos métodos son llamados desde eventos del DataEnvironment como
OpenTables y BeforeOpenTables que se ejecutan antes que Form.Load. Para el nivel instanciado
establecer propiedades es poco fiable hasta que no se ejecute Form.Load.

DataEnvironment con DEClass/DEClassLibrary de usuario para un formulario basado en .SCX


Cuando las propiedades DEClass y DEClassLibrary de un formulario basado en .SCX tienen asignado
un valor que especifique un objeto DataEnvironment de usuario, las cosas son algo diferentes…
1. En tiempo de diseño, cuando da valor a las propiedades DEClass y DEClassLibrary asignando
una clase DataEnvironment con código en su evento Init, o en el Init de alguno de sus
miembros cursor/relation, y ese Init llama a un método de usuario de ese formulario, sólo el
hecho de asignar valor a las propiedades DEClass y DEClassLibrary genera un error "Objeto
no contenido en el formulario" ("Object is not contained in a Form"), para cada llamada al
método de usuario del formulario (THISFORM).

2. En tiempo de ejecución, como demuestra LISAG_DEClass.SCX se genera el mismo error


"Objeto no contenido en el formulario". Puede seleccionar dos veces <Ignore> para que
continúe LISAG_DEClass.SCX.

3. En tiempo de ejecución, como demuestra LISAG_DEClass.SCX, aquellas mismas llamadas a


métodos de usuario de THISFORM que generaban un error cuando eran llamados desde el
DataEnvironment.Init o desde el Init de uno de sus miembros, NO HACEN NADA si son
llamados desde los eventos DataEnvironment.OpenTables o BeforeOpenTables ... los
métodos de usuario agregados al .SCX en la instancia actual son completamente ignorados.

4. En tiempo de ejecución, como demuestra LISAG_DEClass2.SCX, cuando ocurren eventos


como OpenTables and BeforeOpenTables del DataEnvironment, que se disparan después del
DataEnvironment.Init y el Init de sus miembros llaman a métodos de THISFORM, sólo se
ejecuta el código para aquellos métodos que es heredado de la clase padre ... no el código
colocado en los métodos del nivel actual instanciado.
Conclusión
La conclusión es que el código colocado en métodos de la instancia actual del formulario nunca se
ejecuta cuando el método es llamado desde eventos del DataEnvironment. Para confiar en los
comportamientos abstractos del DataEnvironment, debe crear todo el código en la clase
DataEnvironment (jerárquicamente).

Por ejemplo, algo que necesitamos a menudo es colocar SET TALK OFF antes de que se disparen los
eventos del DataEnvironment, esto se debe hacer en DataEnvironment::Init y posiblemente
también en el Init de su clase base Cursor. Para el nivel instanciado la configuración de las
propiedades no es fiable hasta que no se dispare el Form.Load.

¿Dónde ubicar los comandos SET que están limitados a la Sesión privada de datos?
Varios comandos SET de VFP están limitados a la Sesión privada de datos. Puede revisar la lista en
el tópico SET DATASSESION del archivo Ayuda de VFP. Muchos de esos comandos SET afectan los
datos y por tanto necesitan ser ejecutados para cada sesión privada de datos. Además, SET TALK
está limitado a la sesión privada de datos y debe ser establecido lo antes posible, en OFF que es el
valor no-predeterminado para VFP, en el momento en que la sesión privada de datos (formulario)
es instanciado, para eliminar las salidas TALK al _Screen o al formulario.

Sin embargo, teniendo en cuenta las inconsistencias planteadas anteriormente en relación con el
objeto DataEnvironment, ¿Cuál es el mejor lugar para colocar los comandos SET en una sesión
privada de datos, de forma tal que el formulario se instancia correctamente?
Los archivos LISAG_PDS_SETS*.PRGs y sus correspondientes .SCX demuestran la solución.

Lo que nunca te dijo tu madre


Eso depende de si utiliza o no DataEnvironment y en ese caso, si utiliza el DataEnvironment nativo
de VFP o una clase personalizada DataEnvironment.

No utiliza Dataenvironment
Pienso que es la mejor elección, porque es más consistente, y es más fácil de mantener (vea la
siguiente sección de este documento). En ese caso, lo más lógico es colocar los comandos SET de la
sesión privada de datos en el Load del formulario de la más alta jerarquía que establezca la
propiedad DataSession a 2 -Private Data Session. Comience el Load de cada instancia de formulario
con llamada hacia atrás (callback), para asegurarse que los comandos SET quedarán configurados
desde el inicio:
IF NOT DODEFAULT()
RETURN .F.
ENDIF
DataEnvironment nativo en un formulario basado en .SCX

En el formulario de la más alta jerarquía establezca la propiedad DataSession a 2- Private Data


Session.

Agregue un método de usuario para colocar la configuración lógica de los comandos SET para su
sesión privada de datos junto a cualquier otra cosa que necesite que ocurra al inicio del todo de la
ejecución del formulario.
En el nivel instanciado, llame al método de usuario desde los métodos del DataEnvironment
OpenTables o, mi preferido, BeforeOpenTables:
THISFORM.CustomMethod()

Recuerde, cualquier código que coloque en el método de usuario en el nivel instanciado no se


ejecutará, como fue explicado en la sección anterior ... solamente se ejecuta el código colocado en
la(s) clase(s) padre de la instancia actual.

DataEnvironment de usuario con DEClass/DEClassLibrary


Coloque la configuración lógica de los comandos SET para su sesión privada de datos en el Init de la
clase DataEnvironment. Sea consciente de que pudiera tener algunos comandos SET como SET
TALK OFF en el Init de sus clases base Cursor o Relation, ya que estos se ejecutan antes que el Init
de DataEnvironment.

¿Cómo establecer los comandos SET?


Una vez que haya determinado dónde necesita colocar su código para los comandos SET de su
sesión privada de datos, la cuestión es cómo debe escribir ese código. Puede escribir un código
estricto con los comandos SET deseados (como se demuestra en LISAG_PDS_SETs.SCX); pero el
mejor enfoque pudiera ser tener la sesión privada de datos que lea los valores desde la sesión
predeterminada de datos ... frecuentemente se utiliza la misma configuración de comandos SET,
estos se configuran globalmente para la sesión predeterminada de datos cuando inicia su
aplicación (como se demuestra en LISAG_PDS_SETs_Abstract*.SCXs)
Los ejemplos LISAG_PDS_SETs_Abstract*.PRGs y sus correspondientes .SCX demuestran una de
estas técnicas. Cada LISAG_PDS_SETs_Abstract*.PRG instancia un demo (objeto aplicación)
"application object", que contiene un método de usuario GetSETCommandSetting(). Como el
objeto application es instanciado en la sesión predeterminada de datos #1, al llamar a su método
GetSETCommandSetting() DEVUELVE la configuración especificada como lo establece la sesión
predeterminada de datos. Como la sesión privada de datos instancia, pueden establecer sus
comandos SET para que coincidan con aquellos establecidos en la sesión predeterminada de datos
llamado al objeto application global. Aquí está el código esencial del método SetSETCommands de
la clase frmLISAG_PDS_SETs_Abstract en LISAG_PDS_SETs_Abstract.VCX:
LOCAL laSETs[3], luSetting, lcString
laSets[1] = "DELETED"
laSets[2] = "MULTILOCKS"
laSets[3] = "TALK"
FOR EACH lcSet IN laSETs
luSetting = goApplication.GetSetCommandSetting(m.lcSet)
lcString = "SET " + m.lcSet + SPACE(1) + TRANSFORM(m.luSetting)
&lcString
ENDFOR

Utilice DataEnvironment sólo en tiempo de diseño:


Antes de que decida abandonar del todo el uso del DataEnvironment, existe una idea que debe
considerar: Mientras esté diseñando formularios basados en .SCX, utilice DataEnvironment solo
para cuestiones de diseño:
 Arrastrar y soltar un(os) cursor(es) al formulario para crear instantáneamente controles Grid.
 Arrastrar y soltar los campos al formulario para agregar controles cuyos ControlSource ya estarán
definidos y con un ancho aproximado (Width) (si el mapeo de campos (field mapping) tiene
establecido que incluya el título del campo, se puede obtener ya la etiqueta correspondiente al
Caption existente).
 Establecer el ControlSource de cualquier control desde la ventana propiedades, seleccionando uno
los cursores actuales desde el cuadro desplegable.
 Tener acceso al DataEnvironment y sus miembros (cursores) en los generadores de usuarios.
Recuerde establecer las propiedades AutoOpenTables y AutoCloseTables a .F. como muestra la
Figura 5, entonces VFP ignora todo lo que esté en el DataEnvironment en tiempo de ejecución. Sin
embargo, para formularios con sesión privada de datos, cuando el formulario es cerrado/destruido,
VFP cierra todos los cursores abiertos mientras estuvo activo el formulario.
En tiempo de ejecución abra las vistas y tablas manualmente, utilizando una de las técnicas
descritas en el método Load de DesignTimeDE.SCX, aprovechando sus ventajas sobre el
comportamiento del DataEnvironment en tiempo de ejecución:
 Si/cuando hay un problema al abrir una tabla/vista, se puede enviar un mensaje de usuario y
devolver (RETURN) .F., interrumpir la instanciación de esa tabla/vista mientras continúa intacto el
resto de la aplicación. Por el contrario, cuando DataEnvironment encuentra un problema como un
archivo no encontrado, cabecera de tabla dañada, índice dañado, etc. El DataEnvironment falla, se
interrumpe su ejecución, así como todo el resto de la aplicación.
 Se puede establecer el comando SET PATH antes de llamar los datos, de esta forma se puede
intercambiar entre diferentes conjuntos de datos o simplemente ajustar la ruta (PATH) antes de
llamar los datos
 Puede utilizar herramientas como Stonefield Database Toolkit para reparar problemas con tablas,
índices, memos, etc.
Utilizando esta técnica, puede ignorar las inconsistencias por utilizar DataEnvironment, que se han
documentado anteriormente en este documento, ya que no hace nada en tiempo de ejecución.
Incluso en un diseño n-Capas, si sus objetos de Negocio proporcionan un cursor de datos puede
agregar tablas/vistas al DataEnvironment (establecer propiedad Alias), allí donde están disponibles
para conveniencia en tiempo de diseño y son ignoradas en tiempo de ejecución, cuando el objeto
negocio proporciona el dato real.
Esta técnica trabaja igualmente bien para formularios basados en .VCX ... no existe objeto
DataEnvironment nativo, con lo cual tiene que cargar los datos e código desde el método Load y
por tanto ignorar el DataEnvironment.
Si se establecen las propiedades DEClass/DEClassLibrary, los datos indicados están disponibles en
tiempo de ejecución pero el DataEnvironment no está disponible en tiempo de diseño.

Mucho cuidado al romper la secuencia nativa de eventos de instanciación


Existen vías para romper la secuencia nativa de eventos de instanciación. Algunas veces las
consecuencias son menos graves, en otras son catastróficas y pueden causar todo tipo de
comportamiento indeseado.
Lo que tu madre nunca te dijo
El ejemplo LISAG_SetFocus.SCX demuestra una de las posibilidades. Desafortunadamente esto es
muy común y muy fácil de hacer.
La última línea de código en el evento Form.Init, es esta línea aparentemente inocente que asegura
que al instanciar, el botón OK tiene el foco:
THIS.cmdOK.SetFocus()

Sin embargo, cuando ejecute FORM LISAG_SetFocus, verá que el orden de los eventos no es LISAG,
sino LIAGIS como se muestra en la figura 7. Cuando el Init realiza el SetFocus, VFP tiene que activar
inmediatamente el formulario y darle el foco, para que el botón OK pueda tener el foco en ese
punto. Después de esa línea de código, el Form.Init ejecuta cualquier código restante después de
esa línea y continúa con el evento Show.
No muchos formularios dan el SetFocus al primer control de esta forma ... podría simplemente
establecer el comando OK primero en el orden de tabulación. Lo que es más común es establecer el
foco condicionalmente a un control en particular, basado en alguna acción del formulario, basado a
se vez, en un parámetro que reside en el Init. Simplemente he omitido esta condición en el ejemplo
LISAG_SetFocus.SCX para demostrar lo que ocurre cuando se encuentra la condición.

Pero, ¿Cuál es el daño? Eso depende de qué ha codificado en los otros eventos de instanciación del
formulario que normalmente se ejecutan en orden nativo una vez que el Init haya terminado
completamente.

Por ejemplo, puede tener código en lo eventos Show y Activate que se disparan solamente si se
está ejecutando durante la instanciación del formulario (es posible hacerlo por programa - Show()
de un formulario en cualquier momento y el Activate se ejecuta cada vez que el formulario se
convierte en formulario activo, por ejemplo cuando el usuario hace clic en un formulario no modal
abierto en el escritorio VFP) con este fin, agregue una propiedad de usuario (una bandera) con
valor predeterminado a .T. , y lo hace igual a .F. en el Activate o GotFocus. El código a través del
proceso de instanciación pudiera ejecutar condicionalmente sólo si el formulario está siendo
instanciado:
IF THIS.lInstantiating
* realizar estas acciones solo si THISFORM se está instanciando
ENDIF

Ahora, si existe Member.SetFocus(), en el Form.Init la bandera se establece prematuramente en .F.,


antes de que el Init finalice y antes de que se ejecute el Show. Cualquier código en Form.Show que
se debe ejecutar solamente durante la instanciación será ignorado porque la bandera ya está en .F.
en un escenario normal (no una demo) es un error muy difícil de depurar porque solo ocurre si la
condición encuentra el Member.SetFocus() explícito.
El ejemplo demuestra como establecer ese tipo de propiedad de usuario

¿Cuál es la solución?
Entonces, ¿qué se puede hacer en esos casos donde se necesita establecer condicionalmente el
foco a un control particular al instanciar un formulario? El ejemplo LISAG_SetFocus1.SCX demuestra
una técnica. Además de demostrar la idea de una propiedad lInstantiating, utilizada como bandera,
añade un método de usuario InitalSetFocus llamado desde Form.Activate sólo durante la
instanciación. InitalSetFocus proporciona un lugar específico para que el desarrollador ponga su
código para establecer el foco condicionalmente a un miembro en particular de un formulario ... sin
romper la secuencia nativa de los eventos al instanciar.

Cuando al llamar Form.Load() o Form.Init() devuelven .F. no se disparan los eventos


Form.Destroy ni Form.Unload
Devolviendo .F. desde el Load o el Init de un formulario no se instancia, es evidente; pero.

Lo que no te dijo tu madre


1. Como demuestra LISAG_QRDU_AbortLoad.SCX, cuando el Load devuelve .F., no se disparan
ni Form.Destroy ni Form.Unload.
2. Como demuestra LISAG_QRDU_AbortInit.SCX, cuando el Init devuelve .F., se dispara
Form.Unload; pero no lo hace Form.Destroy. Debido a que los miembros contenidos en el
formulario se instancian antes que el Form.Init, el Destroy de esos miembros se dispara ya
que están fuera de límites. (Since form members instantiate before the Form.Init, the
Destroy of form members does fires as they go out of scope).

Bueno, ¿y qué?
Bueno, pues frecuentemente colocamos el código de limpieza en el Form.Destroy, y puede tener
código abstracto en el evento Destroy en la clase base o en otras clases Form que están en la
jerarquía del instanciado actualmente. Lo mismo se puede cumplir para Form.Unload. ¡El código de
limpieza del Cleanup no se ejecuta cuando el Form.Load o el Form.Init devuelven .F. y el código de
limpieza del Cleanup no se ejecuta si el Form.Load devuelve .F.!

Ejecutar Form.Destroy consistentemente


Los formularios LISAG_QRDU_AbortLoadInit1.SCX and LISAG_QRDU_AbortLoadInit2.SCX
demuestran técnicas similares que puede utilizar para garantizar que los Form.Destroy/Unload
(códigos de limpieza) se ejecuten adecuadamente.

Peculiaridad del SYS(1271)


Primeramente vamos a mirar la función SYS(1271) que podemos poner a trabajar para nuestro
beneficio. El ejemplo The LISAG_SYS1271.SCX demuestra que no puede hacer esta llamada:
SYS(1271,THISFORM)

Hasta que se haya completado Form.Load, no se puede llamar desde un método de


DataEnvionment, el Form.Load ni desde otro método llamado desde el Form.Load. De hacerlo,
recibirá el mensaje, poco intuitivo, "Insuficiente memoria para completar esta operación" (" Not
enough memory to complete this operation"). Puede verlo al hacer DO FORM LISAG_SYS1271 ...
seleccionar <Ignore> para continuar con la instalación de LISAG_SYS1271.SCX.
Por un lado, es inconveniente tener que esperar hasta el Form.Load para consultar
SYS(1271,THISFORM) si desea realmente verificarlo antes. Pero LISAG_QRDU_AbortLoadInit1.SCX

Utiliza este comportamiento para determinar cómo debe ser invocado Form.Destroy.

LISAG_QRDU_AbortLoadInit1.SCX
Los eventos Destroy e Init de LISAG_QRDU_AbortLoadInit1.SCX contienen una instrucción IF .F., tal
que si la cambia por IF .T., provoca que el método devuelva .F. La técnica demostrada en
LISAG_QRDU_AbortLoadInit1.SCX es:
1. Al devolver .F. desde Init/Load debido a un failed callback, NO incluya una llamada manual al
Form.Destroy, asumiendo que cada clase padre Form que devuelva .F. desde su código
abstracto lo hará.
2. Al devolver .F. desde Init/Load debido a código en ese nivel, llama THIS.Destroy() antes del
Return .F., asegurándose de que cualquier código de limpieza en el Destroy se va a ejecutar
adecuadamente.
3. El evento Form.Destroy contiene código para llamar a SYS(1271,THISFORM) para determinar
si el Destroy ocurre normalmente (no existe error en SYS(1271)) o debido a llamarlo
explícitamente desde el Form.Load (SYS(1271) genera un error). Al llamar manualmente
desde Form.Load, es llamado el Form.UnLoad.

LISAG_QRDU_AbortLoadInit2.SCX
Los eventos Destroy e Init de LISAG_QRDU_AbortLoadInit1.SCX contienen una instrucción IF .F., tal
que si la cambia por IF .T., provoca que el método devuelva .F. La técnica demostrada en
LISAG_QRDU_AbortLoadInit2.SCX es:
1. Al devolver .F. desde Init/Load debido a un failed callback, NO incluya una llamada manual al
Form.Destroy, asumiendo que cada clase padre form que devuelva .F. desde su código
abstracto lo hará. (igual que LISAG_QRDU_AbortLoadInit1.SCX)
2. Al devolver .F. desde Init/Load debido a código en ese nivel, llama THIS.Destroy() antes del
Return .F., asegurándose de que cualquier código de limpieza en el Destroy se va a ejecutar
adecuadamente. (igual que LISAG_QRDU_AbortLoadInit1.SCX)
3. El Form.Destroy contiene código para verificar PROGRAM(PROGRAM(-1)-1) para determinar
si el Destroy se ha llamado manualmente desde el Form.Load y, en tal caso, llama al
Form.Unload.

Referencia de objetos en la destrucción de formularios. (Object Reference Cleanup On Form


Destruction)

Para que cada objeto se libere / destruya adecuadamente, todas las referencias externas a sus
miembros deben ser liberadas. Esto significa que para cerrar/destruir un formulario, cualquier
objeto externo que mantenga referencia a uno o más miembros debe liberarse explícitamente esa
referencia o establecerse igual a .NULL.

Si algunas de las referencias de objetos no se liberan explícitamente, el contenedor no se libera. Si


el contenedor es un formulario o un contenedor dentro de un formulario, el formulario no se libera.
Esta es la causa del error "Referencia de objeto dañada" ("Dangling object reference").

ORCleanup1.PRG crea 2 instancias de ORCleanup1.SCX para demostrar el problema:


1. DO ORCleanup1
2. En la instancia 2 del formulario, seleccione cualquier casilla de verificación del grupo inferior
para guardar una referencia de objeto a un miembro de la instancia1 del formulario.
3. Intente cerrar la instancia 1: Haga clic en OK, haga clic en "X" a la derecha de la barra de
título, o seleccione Close desde el menú en la caja de control de la barra de título. La
instancia 1 del formulario se niega a cerrarse, debido a referencias de objeto dañadas, de
hecho, si selecciona Cerrar desde el menú en la caja de control de la barra de título, la
opción Cerrar no aparece y la "X" a la derecha de la barra de título aparece deshabilitada.
4. Cierre la instancia 2 del formulario. En cuanto se cierra, se cierra también la instancia1 ... su
código Destroy ya se había disparado, solo estaba esperando a que se liberaran las
referencias de objetos externos a sus miembros.
Lo que no te dijo tu madre
Para formularios, la limpieza de referencia de objetos nunca se debe hacer después de
Form.Destroy. Esto es fácil de hacer.
Sin embargo, cuando los miembros del formulario necesitan limpiar las referencias de objetos, el
código colocado en su Destroy puede ser inútil. Recuerde: el formulario destruye "de afuera hacia
adentro", por tanto el Destroy de los miembros se dispara después del Destroy del formulario
como tal. Cuando se daña la referencia de objetos existente, se dispara el Form.Destroy; pero la
destrucción frena aquí, y no se dispara ningún otro evento, incluyendo el Destroy de sus miembros,
hasta que se libera la referencia de objeto dañada.
Esto es un gran problema cuando diseña formularios de tal forma que un formulario no modal
mantenga referencias de objetos a un miembro de otro formulario no modal u otros objetos
externos. Dos casos comunes de daño de referencia de objetos:
1. Uno o más miembros de Form1 contienen referencias de objeto a miembros de Form2. El
usuario intenta cerrar Form2; pero se niega a cerrar hasta que los miembros de Form1
liberan sus referencias de objeto (este es el escenario que se muestra en
ORCleanup1.PRG/.SCX).
2. Uno o más miembros de Form1 contienen referencias de objeto a miembros de Form2. El
usuario cierra Form1; pero Form1 no se cierra del todo, queda como un objeto artificial
como una sesión de datos "desconocida" ("Unknown") visible en la ventana Sesión de datos.

Cuando objetos externos contienen referencias a miembros de THISFORM


ORCleanup1a.SCX demuestra una vía para solucionar el comportamiento necesario. Repita los
pasos para ORCleanup1 y observe la diferencia:
1. DO ORCleanup1a.
2. En la instancia2 del formulario, seleccione cualquier casilla de verificación del grupo inferior
para guardar una referencia de objeto a un miembro de la instancia1 del formulario.
3. Cierre la instancia 1del formulario. Se cierra normalmente, como se espera, aún cuando
aparentemente no se ha hecho la limpieza de referencia de objetos para liberar las
referencias de la instancia 2 del formulario tiene a los objetos de la instancia 1 del formulario.
Puede ver la técnica que he utilizado en ORCleanup1a.SCX para examinar el código en el evento Clic
de cada casilla de verificación del grupo inferior. He utilizado la función BINDEVENT() para asegurar
que cuando se guarda una referencia de objeto, el Destroy de los objetos guardados del formulario
llama automáticamente al código de limpieza de los objetos haciendo su almacenaje. Entonces,
cuando es cerrado el formulario cuyas referencias a objetos miembros han sido guardadas, su
Destroy llama al código de limpieza de la referencia de objetos de los objetos externos
manteniendo la referencia de objetos. He aquí lo que ocurre:
1. DO ORCleanup1a
2. Haga Clic en la primera casilla de verificación del grupo inferior de la instancia 2 del
formulario. El código del evento Clic guarda una referencia de objeto al miembro txtDemo1
en la instancia 1 del formulario a la propiedad de usuario oFormMember de la instancia 2 del
formulario. El código Clic también ejecuta BINDEVENT() para asegurarse de que cuando se
cierra la instancia 1 del formulario se ejecuta el método ORCleanup de la instancia 2 del
formulario.
3. Haga Clic en el botón de comandos OK de la instancia1 del formulario. Cuando se dispara el
Destroy, ejecuta el método Cleanup de la segunda instancia del formulario, gracias al
BINDEVENT(). Entre otras cosas, el método ORCleanup de la instancia 2 del formulario
establece su propiedad oFormMember a .NULL., liberando la referencia de objeto guardada
el txtDemo miembro de la instancia 1 del formulario.
En un conjunto real de clases jerárquicas, pudiera probablemente agregar el método ORCleanup a
cada una de sus clases bases, de tal forma que el Checkbox.Click haría BINDEVENT() a su propio
método ORCleanup en lugar de THISFORM.ORCleanup, haciendo más granular el control del
proceso.

Esta técnica requiere, por supuesto, utilizar VFP 8.0, versión en la que fue agregada al lenguaje la
poderosa función BINDEVENT().

Cuando los miembros de THISFORM contienen referencias a objetos externos


Como se ha descrito antes, toda la limpieza a las referencias de objeto "garbage collection" debe
hacerse antes de Form.Destroy, que ocurre antes que se dispare el evento Destroy de cualquier
miembro.

La solución es codificar el Form.Destroy con mensajes a sus miembros para establecer


explícitamente todas las referencias de objeto guardadas a .NULL. Mi sugerencia es separar esta
tarea a un método de usuario ORCleanup agregado a sus clases base.
Primero, agregue un método de usuario ORCleanup agregado a sus clases base Form. Agregue
código al Destroy de su clase base formulario para llamar a THISFORM.ORCleanup().
En cada clase formulario o instancia, cada vez que escriba código para guardar una referencia de
objeto a un miembro de formulario, asegúrese de colocar el código de liberación correspondiente
en el ORCleanup del formulario contenido. Cada vez que se dispara el Form.Destroy, es realizada la
limpieza a todas las referencias de objeto.
Sin embargo, puede que quiera abstraerse en lo delante de este comportamiento, ya que cuando
está diseñando la clase contenedora (pageframes, optiongroups, grids, containers, etc.), no puede
colocar código en el Destroy del formulario contenido porque no sabe qué instancia del formulario
o clase va a contener finalmente el contenedor que está preparando. Para solucionar este
problema, agregue un método de usuario ORCleanup para cada una de sus clases base que pueden
ser miembros de un formulario (Textbox, Spinner, Custom, etc.). Programe su clase base form
Form.ORCleanup para interactuar con cada uno de sus miembros, llamando su método ORCleanup.
Si diseña el método ORCleanup de objetos contenedores (grids, pageframes, pages, optiongroups,
containers, etc.) para interactuar con cada uno de sus miembros de la misma forma que el
ORCleanup a nivel de formulario solo hace un lazo entre su arreglo de controles y llama al
ORCleanup de cada uno de sus miembros directos.

Dañar la referencia de objetos cuando se utilizan colecciones


Ver la serie de ejemplos ORCleanup2.SCX
_Screen.ActiveForm y qué se puede hacer con esto
Durante el curso de instanciación del formulario hay un punto en el cual el formulario se convierte
en _Screen.ActiveForm. Sabiendo cuando esto ocurre nos permite poder hacer cosas muy buenas
con la referencia de objeto _Screen.ActiveForm.

Cuando THISFORM se convierte en _Screen.ActiveForm


Como se demuestra en el ejemplo _ScreenActiveForm.SCX, el formulario que se está instanciando
se convierte en _Screen.ActiveForm inmediatamente después de ser mostrados (Show()).

Lo que nunca te dijo tu madre


Desafortunadamente, _Screen.ActiveForm no es siempre _Screen.ActiveForm. En muchas
ocasiones donde el formulario activo contiene uno o más controles ActiveX, _Screen.ActiveForm
puede ser una referencia de objeto a un control ActiveX, NO al formulario que lo contiene.
La biblioteca en tiempo de ejecución X6SAF.PRG para el framework Visual MaxFrame Profesional
está incluida en esta sesión y controla este caso. Para tener una referencia de objeto confiable
_Screen.ActiveForm, sustituya el código por el siguiente:
LOCAL loActiveForm
loActiveForm = _Screen.ActiveForm
IF TYPE("loActiveForm.BaseClass") = "C"
* aquí no es el formulario activo
ELSE
* m.loActiveForm contiene una referencia de objeto fiable al formulario actualmente activo
ENDIF

…lo que es el equivalente más fiable:

LOCAL loActiveForm
loActiveForm = X6SAF()
IF ISNULL(m.loActiveForm)
* aquí no es el formulario activo
ELSE
* m.loActiveForm contiene una referencia de objeto fiable al formulario actualmente activo
ENDIF

Guardar una referencia al formulario y objeto llamados


Al combinar los conocimientos sobre la secuencia de eventos de instanciación de formulario LISAG
y el conocimiento de cuando THISFORM se convierte en _Screen.ActiveForm, hay algunas cosas
interesantes que puede hacer con esta información.
El ejemplo _SAF1.SCX/_ScreenActiveForm1.SCX demuestra una buena técnica para guardar
fácilmente una referencia de objeto al formulario y objeto (si existe) llamados.
Guardar una referencia al formulario llamado
Cuando se ejecuta Form.Load, THISFORM no se ha instanciado todavía, y no es el formulario activo
_Screen.ActiveForm. Más importante, cualquier formulario (si existe) que se ejecuta cuando
THISFORM se llama se refleja aun en _Screen.ActiveForm. Así, es como un pestañeo a obtener una
referencia de objeto "libre" al formulario que se está ejecutando cuando se llama THISFORM, y lo
guarda en una propiedad de usuario disponible durante la vida del THISFORM:
LOCAL loActiveForm
loActiveForm = X6SAF()
IF VARTYPE(m.loActiveForm) = "O"
THIS.oCallingForm = m.loActiveForm
ELSE
THIS.oCallingForm = .NULL.
ENDIF

A partir de ahora, THISFORM, puede "hablar" al formulario llamado via la referencia de objeto
THISFORM.oCallingForm object. THISFORM.Destroy establece en .NULL.
THISFORM.oCallingForm object:

Algunas cosas interesantes sobre estas técnicas:


1. Puede consultar información sobre el formulario llamado antes del THISFORM.Init, donde se
reciben parámetros, aceptando otra configuración que necesita optimizar lo que ocurre
antes de que puedan ser verificados los parámetros.
2. No hay necesidad de llamar al formulario para pasar una referencia de objeto a sí mismo al
formulario llamado.
3. Cuando THISFORM se instancia desde cualquier sitio, como un menú, no hay sitio para el
formulario activo para pasar una referencia de objeto a si mismo a THISFORM.Init.
4. Debido a que la referencia de objeto para el formulario llamado se guarda en una propiedad
de usuario de THISFORM, está disponible durante la vida de THISFORM, luego de
_Screen.ActiveForm se actualiza nunca después de la referencia del formulario llamado.
Estas características se muestran en el ejemplo _SAF1.SCX/_ScreenActiveForm1.SCX como se
muestra en la figura 11:
1. El evento Clic del cmdCallForm en _SAF1.SCX un comando DO FORM ... no se pasan
parámetros.
2. El evento Load del _ScreenActiveForm1.SCX contiene código para verificar el formulario
llamado, y entonces, guarda una referencia de objeto a su THISFORM.oCallingForm.
3. El evento Init del grdCustomers en _ScreenActiveForm1.SCX contiene código para verificar el
formulario llamado. En tal caso este formulario es consultado por su actual
Orders.CustomerID, en cualquier caso, como se muestra en la figura 11. En ese caso
grdCustomers establece el puntero a su registro inicial al CustomerID indicado. Observe que
esta aplicación tiene lugar antes que THISFORM.Init, donde un parámetro CustomerID
pudiera ser recibido y aplicado.

Puede DO FORM _ScreenActiveForm1.SCX con nada, y el código descrito arriba simplemente


encuentra que no hay mucho que puedas hacer.

¡Abstraerlo!
El comportamiento que ve en Load, Destroy, y ORCleanup puede abstraerse simplemente en su
formulario base clase. Todos los formularios que heredarán esta característica cada instancia lo
utilicen o no.

Guardar una referencia a un control del formulario llamado


Al guardar una referencia de objeto de un formulario existente es fácil. Pero, ¿qué tal si guardamos
una referencia de objeto al control actual en el formulario; pero sólo si este control está en la pila
de ejecución del programa, indicando que es responsable por llamar THISFORM (como el botón
cmdCallForm en _SAF1.SCX)?

Esto toma un poco más de trabajo, puede fácilmente abstraerse de tal forma que esté disponible
para todos los formularios _ScreenActiveForm1.SCX contiene el código necesario:
1. Load contiene un código que verifica la pila de ejecución del programa para la llamada del
control en el formulario llamado, si se encuentra uno, se guarda una referencia en
THISFORM.oCallingFormControl para utilizarlo mientras exista THISFORM.
2. Como se ha explicado previamente en este documento, en cualquier momento puede
guardar una referencia de objeto para un miembro de objeto, debe proporcionar para la
limpieza de la referencia de objeto. El Load de _ScreenActiveForm1.SCX hace que
BINDEVENT() enlaza el Destroy del formulario llamado con THISFORM.ORCleanup. Si
_ScreenActiveForm1.SCX es modal, esta acción no se requiere actualmente, porque
THISFORM se puede cerrar antes de que el formulario puede ser llamado.
3. El Destroy de _ScreenActiveForm1.SCX llama a su ORCleanup de usuario.
4. El ORCleanup de_ScreenActiveForm1.SCX libera explícitamente las referencias de objeto
oCallingForm y oCallingFormControl. Cuando THISFORM es no modal, se dispara el Destroy
del formulario llamado THISFORM.ORCleanup gracias al BINDEVENT() en THISFORM.Load.

Observe que la referencia de objeto del formulario llamado, si este control es realmente llamado
por el formulario llamado, se guarda sólo en THISFORM.oCallingFormControl. Por ejemplo, si se
ejecuta un formulario cuando se llama un segundo formulario desde otro lado como una opción de
menú, el control activo en el formulario activo no llama al segundo formulario.
THISFORM.oCallingFormControl no se guarda debido a que un método del control activo en el
formulario activo no está en la pila de ejecución del programa. Entonces, como se ha explicado en
el comentario del código al final del método Load de _ScreenActiveForm1.SCX, puede decirlo
fácilmente si THISFORM.oCallingForm es llamado por THISFORM, o fue simplemente el formulario
llamado cuando es llamado THISFORM.
Estas características se demuestran en el ejemplo _SAF1.SCX/_ScreenActiveForm1.SCX, como
muestra la Figura 12:
1. El Init de _ScreenActiveForm1.SCX determina sus posiciones Top y Left relativos al control
llamado, si existe. Las referencias de objeto a un formulario llamado y su control llamado
podría pasar al Init, en lugar de utilizar la referencia de objeto THIS.oCallingFormControl
guardada en Load. Sin embargo, esto requiere que el desarrollador que codifica la llamada al
formulario recordando que pase siempre los parámetros necesarios a
_ScreenActiveForm1.SCX, y en el orden correcto.
2. El AfterRowColChange de grdCustomers en _ScreenActiveForm1.SCX actualiza el Caption del
botón del formulario llamado. Esto es solamente por propósitos de diversión / demo, para
mostrar cual fácil es "hablar" al control del formulario llamado.
3. El ORCleanup de _ScreenActiveForm1.SCX contiene código para cambiar el Caption del
botón del formulario llamado.
Puede además hacer DO FORM _ScreenActiveForm1 con nada, y el código escrito antes encuentra
que no hay mucho que hacer.

¡Abstraerlo!
El comportamiento que ve en el Load, Destroy, y ORCleanup se puede abstraer fácilmente en su
clase base formulario. Todos los formularios heredan esta característica aunque la utilice o no la
instancia indicada.

Mantener referencias de objetos a los formularios llamados


El ejemplo _SAF1.SCX/_ScreenActiveForm1.SCX descrito en la sección previa pudiera hacerle
pensar sobre una técnica más poderosa. En ese caso, seguramente disfrutará de esta.
Me han preguntado por un código para situaciones donde el formulario no modal necesite no sólo
llamar a uno o más formularios no modales adicionales; pero además para cada uno de los
formularios llamados para mantener las referencias de objeto a cada otro así que puede actualizar
cada otro de tiempo en tiempo.
Con más frecuencia me han preguntado algunos desarrolladores que han tratado de implementar
una situación, como para depurarlo. No es trivial, mantener todas las referencias de objeto en
ambas direcciones, ni para asegurarse que esa limpieza de referencia de objeto (garbage collection)
fue hecha para prevenir una posibilidad de dañar la referencia de objeto.
Gracias a la función BINDEVENT() agregada en VFP 8.0, este escenario es ahora muy fácil de
implementar. Mejor aún, el código necesario es fácil de conceptuar. La serie de ejemplos
FormORCleanupCaller*.SCX contienen el código. Todos los formularios en la serie
FormORCleanupCaller*.SCX están basados en el ejemplo de la clase base formulario frmBaseForm
en la biblioteca FormORCleanup.VCX.
La técnica es manipular todo sin pasar ningún parámetro, utilizando una técnica similar demostrada
en el ejemplo _SAF1.SCX/_ScreenActiveForm1.SCX.
Aquí vemos los conceptos en frmBaseForm:
1. La llamada desde Load llama al método de usuario StoreCalledForm.
2. La llamada desde StoreCalledForm verifica si existe un formulario llamado. En ese caso:
a. La referencia de objeto a un formulario llamado desde cualquier objeto llamado es
guardada en THISFORM.oCallingForm y THISFORM.oCallingFormControl.
b. Un BINDEVENT() es utilizado para asegurar que cuando se cierra el formulario llamado,
es llamadoTHISFORM.ORCleanupCallingForm, donde las referencias de objeto se
guardan en THISFORM.oCallingForm y son liberados THISFORM.oCallingFormControl,
permitiendo que el formulario llamado cierre adecuadamente.
c. Si el formulario llamado tiene un método de usuario StoreCalledForm (será si hereda
desde frmBaseForm), es llamado su método StoreCalledForm y se pasa una referencia
de objeto a THISFORM.
3. La llamada desde StoreCalledForm:
a. Guarda una referencia de objeto al formulario llamado en una propiedad de arreglo
(Tuve problemas de hacer una propiedad colección para trabajar adecuadamente).
b. Utilice un BINDEVENT() para asegurar que cuando se cierra el formulario llamado (el
que está actualmente bajo instanciación), el método ORCleanupCalledForm del
formulario llamado es llamado, donde es liberada la referencia de objeto al formulario
llamado.
El resultado en cadena es que ambas llamadas y llamados formularios mantengan la referencia de
objetos entre ellos. Cada formulario llamado puede tener una referencia de objeto a sólo un
formulario invocador; pero cada formulario invocador puede mantener la referencia a un número
ilimitado de formularios invocados. Más importante, es manipulada la necesaria limpieza de
referencia de objeto.
Cada instancia de formulario hereda todo el comportamiento necesario, y el formulario o sus
miembros puede simplemente THISFORM., THISFORM.oCallingFormObject, y
THISFORM.aCalledForms[] objetos en cualquier momento (por supuesto, después de verificar para
ver si son referencias válidas de objeto)
Este comportamiento se demuestra de esta forma, como se observa en las figuras 14 y 15:
1. DO FormORCleanupCaller1.SCX
2. Haga Clic en cualquiera de los botones DO FORM. Si hace Clic en alguno de ellos más de una
vez, se cargan nuevas instancias de FormORCleanupCalled*.SCX sobre las ya existentes.
3. Haga Clic en el botón <?> de cualquier formulario para ver en cada formulario sobre el otro
invocador/invocado.
4. Haga Clic en el botón <OK> de cualquier formulario para que vea que no hay problemas de
referencias de objetos dañadas.

FormORCleanupCaller2.SCX
El ejemplo FormORCleanupCaller2.SCX es el mismo que FormORCleanupCaller1.SCX, excepto que
cuando el formulario invocador es cerrado, todos los formulario que llama se cierran
automáticamente. Encontrará el código para esto en el evento Destroy.

Nota: El autor ha dado su autorización, y los ejemplos se pueden descargar


de: DrewSpeedieDemo.zip (167 KB).
Controlar datos en buffer en Visual FoxPro
Artículo original: Handling buffered data in Visual FoxPro
http://weblogs.foxite.com/andykramek/2005/12/27/handling-buffered-data-in-visual-foxpro
Autor: Andy Kramek
Traducido por: Ana María Bisbé York

Un par de meses atrás escribí sobre buffer; pero deliberadamente dejé fuera de la discusión dos
funciones vitales mientras trabajaba con buffer- nombradas TableUpdate() y TableRevert(). Estas
son la base mediante la cual usted, el desarrollador, controla la transferencia de los datos entre el
buffer de Visual FoxPro y el origen de datos originales. La función TableUpdate() toma los datos
pendientes desde el buffer y los actualiza en la tabla original, mientras TableRevert() refresca los
buffers para releer el dato desde el origen de datos. La realización exitosa de otras funciones da
como resultado un buffer 'limpio' lo que significa que, para Visual FoxPro, los buffer y el origen de
datos son sincronizados.

Controlar el alcance de las actualizaciones


Debido a que Visual FoxPro admite tanto buffer de Filas y de Tablas, ambas funciones de traspaso
de datos pueden operar "sólo sobre el registro actual" o sobre "todos los registros modificados". El
primer parámetro que se pasa a la función determina el alcance de la operación y ufff tenemos otra
confusión posible aquí. Las funciones operan en modo significativamente diferente y tienen tipos
diferentes de valores devueltos.

En la versión 3.0 de Visual FoxPro; ambas TableUpdate() y TableRevert() aceptarían solamente un


parámetro lógico para determinar el alcance del cambio que están controlando. Pasar un valor
de .T., significa que todos los cambios pendientes fueron procesados, mientras .F. restringe las
operaciones al registro actual solamente, sin importar el modo de buffer empleado.

TableRevert() devuelve el número de filas que fueron revertidos y no pueden realmente "fallar" -
sin que fuera un problema físico, como perdiendo una conexión de red. En una fila de tabla en
buffer, o cuando específicamente el alcance como .F., el valor devuelto, por tanto siempre 1.

TableUpdate() devuelve un valor lógico que indica si la modificación especificada es exitosa,


independientemente del alcance. En otras palabras, un valor devuelto de .T., indica que todos los
registros en el alcance han sido actualizados con éxito. Sin embargo, el comportamiento cuando se
utiliza un parámetro lógico para determinar el alcance y la actualización falla por cualquier razón,
es que no se genera un error y la función devuelve un valor de .F. dejando el puntero en el registro
en que falla.

Si está actualizando un solo registro, esto es muy sencillo; pero si está actualizando múltiples
registros en una tabla, y un registro no puede ser actualizado, esto significa que cualquier
actualización posterior no puede ser verificada. Pero, después de solucionar el conflicto para el
registro que ha fallado, no hay garantía que re-intentar la actualización que no va a fallar al registro
más cercano. Esto puede ser un problema!

El comportamiento de TableUpdate() fue, por tanto, modificado en la versión 5 para


aceptar incluso un parámetro numérico o lógico para el alcance, donde 0 es equivalente para .F. y 1
para .T. El nuevo comportamiento, que puede ser solamente especificado al pasar "2" como
parámetro de alcance, específicamente dirigidos los problemas de actualización de múltiples
registros.

Al utilizar buffer de tablas, al llamar TableUpdate() con un parámetro de alcance de "2" Visual
FoxPro intenta actualizar todos los registros que tienen cambios pendientes. Sin embargo, si un
registro no puede ser actualizado, en lugar de parar, la función registra el número de registro que
ha fallado en una matriz (cuyo nombre puede ser especificado como el cuarto parámetro) y
continúa tratando de actualizar otros registros cambiados. La función devuelve .F.
si cualquier registro falla la actualización; pero al menos tratará de actualizar todos los registros
disponibles. La matriz de salida contiene una lista de los números de registros para aquellos
registros fallan la actualización.

El segundo parámetro para TableUpdate()


Una diferencia mayor entre la sintaxis de TableUpdate() y TableRevert() es que el anterior puede
solamente tomar un parámetro extra lógico, en la segunda posición en la lista de parámetros. Esto
controla la forma en la que se comportan las actualizaciones cuando encuentran un conflicto.

De forma predeterminada, Visual FoxPro debe rechazar un cambio cuando un conflicto entre el
buffer de datos y se detecta el dato original (vea debajo un debate completo del conflicto y la
resolución). Al especificar un valor lógico .T. como el segundo parámetro puede forzar una
actualización a que sea aceptada incluso en situaciones cuando debería fallar. Naturalmente esto
es algo que deseará hacer de forma predeterminada; pero existen, como veremos luego,
situaciones donde este comportamiento no es solamente deseado, sino esencial.
Especificar la tabla a ser actualizada o revertida
Ambas funciones TableUpdate() y TableRevert() operan sobre una tabla al mismo tiempo. El
comportamiento predeterminado es que, a menos que específicamente, se determine lo contrario;
actuará en la tabla en el área de trabajo seleccionada. Si no hay una tabla abierta en el área de
trabajo, hay un error (No se encuentra el alias - Error 13). Ambos, sin embargo, pueden actuar
sobre una tabla abierta disponible en la sesión de datos actual y puede aceptar cualquiera de los
nombres de ALIAS (el tercer parámetro para TableUpdate(), segundo para TableRevert()) o un
número de área de trabajo.

No recomendamos el uso del número del área de trabajo en esto, o cualquiera, situación donde está
especificando una tabla diferente que la seleccionada. Tal y como hemos podido ver esta
funcionalidad está incluida, sólo por compatibilidad hacia atrás y no tiene lugar en el entorno VFP.
Existen dos razones para evitar el uso del área de trabajo. Primeramente, hace su código
dependiente de tablas específicas estén abiertas en áreas específicas de trabajo - lo que es aun
mayor limitación si piensa cambiar! En segundo lugar, no tiene control real sobre las tablas abiertas
en VFP, los cursores o vistas cuando utiliza de cualquier forma el Entorno de datos del formulario.
Entonces, liberar el número del área de trabajo en lugar del alias, es una estrategia muy arriesgada
e innecesaria.

El único momento que recomendamos el uso del número de área de trabajo es cuando guardamos
el área de trabajo actual guardando el valor devuelto de la función SELECT(0). Utilizar el número del
área de trabajo en este caso asegura que el área de trabajo actual está vacía, o que las tablas que
contienen se cierren durante cualquier operación que esté haciendo, puede aun devolverlo sin error.

Conclusión
Existe mucha funcionalidad y flexibilidad oculta dentro de TableUpdate() y TableRevert(). Al utilizar
buffer, necesita tener cuidado de, exactamente cuáles de varias combinaciones de sus parámetros,
puede pasarles para asegurar que está utilizando la combinación correcta que necesita.
Mientras TableRevert() es bastante simple, TableUpdate() es más compleja y por eso, en la Tabla 2
que muestro debajo se brinda un resumen de algunas combinaciones "prácticas" de parámetros
para TableUpdate().
Tabla 2. Opciones de TableUpdate()

Parámetros

Alcance Fuerza Tabla Salida Acción


0 ó .F. .F. Intenta actualizar la fila actual del alias actual
0 ó .F. .T. Fuerza la actualización del registro actual solamente del alias actual
0 ó .F. .F./.T. Alias Intenta forzar todo los registros disponibles de las alias especificadas
1 ó .T. .F. Intenta actualizar la fila actual sólo del alias especificado
1 ó .T. .T. Fuerza la actualización de todos los registros disponibles del alias actual
Intenta actualizar la fila actual sólo del alias especificado. Se detiene en un
1 ó .T. .F./.T. Alias
fallo
Intenta actualizar todos los registros disponibles del alias especificado.
2 .F. Alias Matriz
Nota los fallos; pero no se detiene.
Fuerza la actualización de todos los registros disponibles del alias
2 .T.
actual/especificado
Intenta/Fuerza la actualización de todos los registros disponibles del alias
2 .F./.T. Alias Matriz
especificado. Nota los fallos; pero no se detiene.

Detectar y solucionar conflictos


La sección anterior trataba sobre los mecanismos para actualizar una tabla con buffer y en mi
artículo anterior, recomendé que el Buffer de Tabla optimista debe ser la opción normal para la
mayoría de las aplicaciones. Entonces, el próximo problema está, en asegurarse de que los cambios
no son la causa de un conflicto de actualización cuando guarda. Uno de los problemas inherentes al
utilizar bloqueo optimista en un entorno multiusuario es que es posible para más de un usuario
hacer cambios al mismo registro al mismo tiempo. Puede preguntarse ¿porqué este tipo de cosa
pudiera ser posible alguna vez?- ¿ está seguro de que dos usuarios no pudieran actualizar el mismo
registro al mismo tiempo?

En la práctica, existen muchas situaciones donde puede ocurrir legítimamente. Considere la


situación en una Orden de Ventas en el procesamiento del sistema. Cuando se coloca una orden
para un elemento, el "stock disponible" actual debe reajustarse para reflejar la reducción. Si dos
usuarios, que son controlados por dos operadores simultáneamente, incluyen el mismo elemento
en sus órdenes, existe un gran posibilidad de que surja un conflicto. Obviamente, esto puede no
ocurrir si el sistema utiliza bloqueo pesimista; pero esto tiene otras consecuencias, generalmente
indeseables. En este caso, el segundo operador, que trata de acceder al elemento en cuestión,
recibirá un mensaje que el registro está en uso por alguien y no puede hacer modificaciones - no es
mucha ayuda ! Aún más, el bloqueo pesimista puede ser utilizado cuando una tabla Visual FoxPro
es utilizada directamente como origen de dato - no puede bloquear de forma pesimista una vista
de cualquier tipo (ni para datos locales o remotos).

Al utilizar buffer, Visual FoxPro hace una copia de todos los datos como e recuperan de la tabla
física, cuando se pide una actualización, se compara esa copia con el estado actual del dato. Si no
hay cambios, la actualización es permitida, en otro caso, la actualización se genera un error de
conflicto (#1585 para las vistas, #1595 para tablas).

El rol de OldVal() y CurVal()


La base de toda la detección del conflicto está en dos funciones nativas, OldVal() y CurVal(), las que
acceden a los cursores intermedios creados por Visual FoxPro cuando utiliza datos en buffer. Como
indican sus nombres, OldVal() devuelve el valor de un campo tal y como era cuando el usuario final
leyó el dato del origen, mientras CURVAL() devuelve el estado actual del dato en la tabla origen.
Estas dos funciones operan desde el nivel de campo y, aunque ambas pueden aceptar una
expresión que evalúa una lista de campos, son más usadas para devolver valores de campos
individualmente para evitar el problema de solucionar diferentes tipos de datos.

Aquí hay un problema al utilizar CurVal() para verificar el estado de la vista. Visual FoxPro
realmente mantiene un nivel adicional de buffer para una vista, la cual, a menos que sea refrescada
inmediatamente antes de verificar el valor del campo, puede causar CurVal() que devuelve la
respuesta incorrecta.

Entonces, ¿Cómo puedo realmente detectar el conflicto?


Antes de entrar en la discusión de cómo detectar un conflicto, permítanme ser claro sobre la
definición de Visual FoxPro sobre lo que es un conflicto. Como he comentado antes, en la discusión
sobre buffering, Visual FoxPro realmente hace dos copias de un registro, cada vez que el usuario
accede al dato. Una copia está disponible como cursor modificable y es donde un usuario hace los
cambios. La otra copia guarda el estado original. Antes de permitir la actualización, Visual FoxPro
compara este cursor original con el dato guardado actualmente en el disco. Ocurre un conflicto
cuando estas dos versiones del dato no coincide exactamente, y existen dos formas en las que
puede ocurrir. La primera, y más obvia, es debido a que el usuario actual hace cambios a un
registro y trata de guardar esos cambios después de que otro ha cambiado y guardado el mismo
registro.

La segunda es menos obvia y ocurre cuando un usuario hace un cambio; pero entonces los cambios
van a sus valores originales. El resultado es que no cambia cuando en realidad hace; pero cuando
tratan de "guardar" el registro Visual FoxPro va a ver aun esto como un conflicto debido a que los
valores OldVal() y CurVal(), en realidad son diferentes. Para evitar este tipo de error puede
simplemente confiar en GetFldState(); pero debe comparar expresamente los valores en el buffer
actual con estos en OldVal().

Entonces, habiendo evitado la posibilidad de conflictos cuando el usuario actual no ha hecho


ningún cambio; pero sencillamente no trata de confirmar el registro, existen básicamente dos
estrategias que puede adoptar para detectar conflictos. La primera es el proceder "Trate y vea".
Esto significa simplemente que no trata y detecta conflictos potenciales; pero sólo atrapa el
resultado de una llamada para TableUpdate() y cuando falla, determina porqué.

El segundo proceder es "Cinturones y tirantes" en el que cada campo cambiado se verifica


individualmente y cualquier conflicto se soluciona antes de intentar actualizar la tabla original.
Mientras esto parece más defensivo, y por tanto "mejor" en realidad oculta un problema. En el
tiempo que esto ocurre (aunque sea muy pequeño) para verificar todos los cambios contra sus
valores actuales, otro usuario puede cambiar exitosamente el mismo registro que está verificando.
Entonces, a menos que también haya bloqueado explícitamente el registro antes de comenzar a
verificar los valores, la actualización real pudiera fallar. Debido a que queremos realmente evitar
explícitamente la colocación de bloqueos, necesita incorporar exactamente la misma verificación
del resultado de TableUpdate(), y brinda el mismo control del fallo, lo que es mucho más simple
precisamente la estrategia "Intenta y veremos".

Por tanto, a menos que tenga una razón para sobreescribir cambios pre-validados, recomendamos
fuertemente que permita que Visual FoxPro detecte los conflictos y justamente atrápelo para
aquellos errores que han surgido.

De acuerdo, entonces, habiendo detectado un conflicto de actualización, ¿qué puedo hacer sobre
esto?
He aquí cuatro estrategias básicas para actualizar conflictos. Puede escoger una, o combine más de
una en una aplicación en dependencia de la situación real:
[1] El usuario actual siempre gana - Esta estrategia es apropiada solamente en aquellas situaciones
en las que se asume que son correctos los datos del usuario que está actualmente intentando
guardar. Típicamente esto debería ser implementado en la base del ID del usuario, el que es en
realidad está haciendo el guardado y debería implementar una regla de negocio que cierta
información de una gente es más útil que otra.

Un ejemplo podría ser tener una aplicación donde un operador hable al usuario podría tener
derechos de sobreescritura para contactar información para el cliente (en la base que la persona
realmente habla al cliente es más probablemente capaz de obtener los detalles correctos). El
conflicto puede surgir en este caso cuando un administrador está actualizando un detalle de cliente
desde un dato en archivo o última orden, mientras un operador tiene detalles nuevos,
directamente desde el cliente. La implementación de esta estrategia en Visual FoxPro es muy
sencilla. Simplemente establezca el parámetro "FORCE", (el segundo) en la función TableUpdate() a
".T." y reintente la actualización.

[2] El usuario actual siempre pierde - Esto es exactamente lo contrario a la anterior. Al usuario
actual se le permite solamente guardar los cambios siempre que no hay otro usuario que haya
hecho cambios. Por el contrario, será implementado normalmente en base al ID del usuario y
podría reflejar la probabilidad que este usuario en particular es propenso a trabajar con
información "histórica" en lugar de información "actual". La implementación en Visual FoxPro es
también muy sencilla. Los cambios del usuario actual se revierten, se recarga la información original
y el usuario tiene que hacer cualquier cambio que necesite, una vez más. Esta es, probablemente la
estrategia que es adoptada más frecuentemente - pero usualmente en base global.

[3] El usuario actual gana a veces - Esta estrategia es la más compleja de las cuatro a implementar;
pero es en la actualidad, bastante frecuente. El principio básico es que cuando ocurre un conflicto
de actualización, usted determina si alguno de los campos, que el usuario actual ha cambiado, van
a afectar los cambios hechos por otro usuario. Si no, el registro del usuario actual es actualizado
automáticamente (utilizando el valor de CURVAL()), entonces esto provoca que es negado el
conflicto y la actualización se reintenta. Sin embargo, debido a no puede cambiar los valores
devueltos por OldVal(), necesita forzar la segunda actualización.
Incidentalmente, esta estrategia también está dirigida al problema de cómo controlar el conflicto
de actualización "falso positivo". Esto ocurre cuando existen discrepancias entre los valores del
disco y aquellos en el buffer de usuario, pero los actuales cambios del usuario, no crean en realidad
un conflicto con cualquier otro cambio que se haya hecho. Claramente, esta situación no es en
realidad un conflicto; pero necesita ser controlada.

Aunque no es trivial, la implementación en Visual FoxPro es relativamente fácil. Primero, la


función CURVAL() es utilizada para determinar cómo actualizar el buffer de usuario actual de tal
forma que no va sobreescribir los cambios hechos por otro usuario. Entonces, la actualización se
aplica utilizando el segundo parámetro FORCE en el TableUpdate() para decirle a Visual FoxPro que
ignore el conflicto que van a surgir porque OldVal() y CurVal() no corresponden.

[4] El usuario actual decide - Este es el caso de "Coge todo". El conflicto no falla bajo ninguna regla
de negocio reconocida, así que la única solución es preguntarle al usuario cuya acción de guardar
desencadena el conflicto que es lo que desea hacer. La idea básica es que usted muestre al usuario
que ha desencadenado el conflicto con una lista de valores - aquellos que acaba de entrar) y el
valor que hay ahora en la tabla ( es decir con el cual alguien ha cambiado el valor original). El
usuario puede entonces decidir si hay que forzar o revertir sus propios cambios. Las herramientas
básicas para la implementación de esta estrategia han sido discutidos en secciones anteriores.
Todo lo requerido es determinar qué cambios traen conflicto y los presentan al usuario como vía
para que el usuario pueda decidir en un campo a campo qué hacer, en base al conflicto. En la
práctica, esta estrategia en general se combina con la estrategia [3] mostrada antes, así al usuario
sólo se le presenta una lista de campos donde hay un conflicto en los campos que han modificado
por ellos mismos.

En el próximo artículo de esta serie, veré el diseño e implementación de una clase que controle un
conflicto que puede ser arrastrada a un formulario.

Das könnte Ihnen auch gefallen