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:
- Evento Form.DataEnvironment.OpenTables (Tablas/vistas en el Entorno de datos DataEnvironment no están en uso USE() o abiertas)
- Evento Form.DataEnvironment.BeforeOpenTables (Tablas/vistas en el Entorno de datos DataEnvironment no están en uso USE() o abiertas)
- Evento Form.Load (Tablas/vistas en el Entorno de datos DataEnvironment están en uso USE() o abiertas)
- Evento Init de cualquier objeto cursor en DataEnvironment
- Evento Form.DataEnvironment.Init
- Evento Init de cada miembro del formulario que es instanciado
- Evento Form.Init
- Evento Form.Show
- Evento Form.Activate
- Evento When del primer control del formulario en el orden de tabulación (tab order)
- Evento Form.GotFocus
- Evento GotFocus del primer control del formulario en el orden de tabulació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:
- Evento Form.QueryUnload
- Evento Form.Destroy
- Evento Destroy para cada uno de los miembros del formulario.
- Evento Form.Unload (Tablas/vistas en el Entorno de datos DataEnvironment están en uso USE() o abiertas)
- Evento Form.DataEnvironment.CloseTables (Tablas/vistas en el Entorno de datos DataEnvironment no están en uso USE() o abiertas)
- Evento Form.DataEnvironment.Destroy
- Evento Destroy para cada cursor en el DataEnvironment
- Evento Form.Release
- Evento Form.Destroy
- Evento Destroy para cada uno de los miembros del formulario.
- Evento Form.Unload (Tablas/vistas en el Entorno de datos DataEnvironment están en uso USE() o abiertas)
- Evento Form.DataEnvironment.CloseTables (Tablas/vistas en el Entorno de datos DataEnvironment no están en uso USE() o abiertas)
- Evento Form.DataEnvironment.Destroy
- Evento Destroy para cada cursor en el DataEnvironment
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
- 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.
- 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.
- Al destruir un formulario no se disparan los eventos Form.Deactivate ni Form.LostFocus
- El DataEnvironment abre sus datos implícitamente entre el DataEnvironment.BeforeOpenTables y el Form.Load() ... inmediatamente antes del Form.Load.
- 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.
- El evento BeforeOpenTables no se dispara antes (Before) que el evento OpenTables, BeforeOpenTables está mal nombrado y se debió llamar algo como AfterOpenTablesBeforeImplicitOpenTables. (DespuesAbrirTablasAntesImplicitamenteAbrirTablas)
- El DataEnvironment.Cursor.Init se dispara DESPUÉS que el cursor ya fue abierto y DESPUÉS del Form.Load
- 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.
- 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.
- 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.
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…
- 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).
- 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.
- 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.
- 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.
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…
- 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).
- 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
- 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.
- 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.
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.
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.
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 ENDIFAhora, 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, 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.
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.
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.
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.
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.
- 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.
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. ENDIFA 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.
- 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.
¡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
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.
¡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.
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.
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).
enlace caido...
ResponderBorrarListo, corregido.
BorrarEl cambio de la URL de los enlaces a archivos cambió el 13-09-2021 por una nueva política de seguridad de Google Drive.