Lo que nunca te contó tu madre sobre instanciar y destruir formularios
03/03/2020
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.
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:
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
- Como demuestra LISAG_QRDU_AbortLoad.SCX, cuando el Load devuelve .F., no se disparan ni Form.Destroy ni Form.Unload.
- 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:
- 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á.
- 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.
- 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:
- 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)
- 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)
- 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:
- DO ORCleanup1
- 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.
- 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.
- 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:
- 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).
- 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:
- DO ORCleanup1a.
- 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.
- 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:
- DO ORCleanup1a
- 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.
- 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:
- 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.
- No hay necesidad de llamar al formulario para pasar una referencia de objeto a sí mismo al formulario llamado.
- 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.
- 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:
- El evento Clic del cmdCallForm en _SAF1.SCX un comando DO FORM ... no se pasan parámetros.
- 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.
- 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:
- 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.
- 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.
- El Destroy de _ScreenActiveForm1.SCX llama a su ORCleanup de usuario.
- 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:
- 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.
- 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.
- 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:
- La llamada desde Load llama al método de usuario StoreCalledForm.
- La llamada desde StoreCalledForm verifica si existe un formulario llamado. En ese caso:
- La referencia de objeto a un formulario llamado desde cualquier objeto llamado es guardada en THISFORM.oCallingForm y THISFORM.oCallingFormControl.
- 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.
- 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.
- La llamada desde StoreCalledForm:
- Guarda una referencia de objeto al formulario llamado en una propiedad de arreglo (Tuve problemas de hacer una propiedad colección para trabajar adecuadamente).
- 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:
- DO FormORCleanupCaller1.SCX
- 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.
- Haga Clic en el botón <?> de cualquier formulario para ver en cada formulario sobre el otro invocador/invocado.
- 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)
Vea también
Referencias
Artículo original: What Your Mother Never Told You About Form Instantiation and Destruction (DevEssentials 2004)
Autor: Drew Speedie
Traducido por: Ana María Bisbé York para PortalFox