12 de marzo de 2021

Enlazar eventos para obtener mejores aplicaciones - Parte 2 de 4

Artículo original: Bind Events for Better Applications
(Bind Events for Better Applications 2017.pdf)
Autor: Tamar E. Granor
Traducido por: Luis María Guayán


... Continuación de: "Enlazar eventos para obtener mejores aplicaciones - Parte 1 de 4"

El seguimiento de los cambios de usuario

Las aplicaciones que realmente se comportan bien habilitan y deshabilitan los controles de forma dinámica, teniendo en cuenta la actividad del usuario. Una característica muy común es mantener el botón Guardar desactivado hasta que el usuario realmente cambie un registro. Para hacer eso, por supuesto, necesita una forma de saber que el usuario ha cambiado los datos.

En la aplicación Biblioteca, la clase de nivel superior para cada control que permite que los datos cambien (como el TextEdit y el TextBox) tiene una propiedad personalizada, lNoteChange. Cuando la propiedad es .T., indica un cambio en el valor de un control en particular que debe ser señalado al formulario, y que se deben tomar las medidas adecuadas. Además, cada control tiene un código y un método AnyChange personalizado en InteractiveChange y ProgrammaticChange que genera el evento AnyChange; ese código se muestra en el Listado 9.

Listado 9. Para poder seguir de cerca los cambios del usuario, agregue un método AnyChange personalizado a cada clase de control y coloque este código en InteractiveChange y ProgrammaticChange.

RAISEEVENT(This,"AnyChange")

La clase formulario de nivel superior también tiene un método AnyChange personalizado, así como uno llamado BindControlEvents. BindControlEvents vincula recursivamente el método AnyChange de nivel de control al método AnyChange de nivel de formulario, como en el Listado 10.

Listado 10. Este código vincula los cambios en los controles a un método de formulario, por lo que el formulario puede reaccionar.

* Bind events of controls to events of the form as appropriate
LPARAMETERS toControl
LOCAL oControl
FOR EACH oControl IN toControl.Objects
 IF PEMSTATUS(oControl, "lNoteChange", 5) AND oControl.lNoteChange
 BINDEVENT(oControl, "AnyChange", This, "AnyChange")
 ENDIF

 IF PEMSTATUS(oControl, "Objects", 5)
 This.BindControlEvents(oControl)
 ENDIF
ENDFOR

La clase base Formulario tiene una propiedad personalizada, lNoteUserChanges, que determina si se llama a BindControlEvents en el método Init del formulario. Está configurado en .F. en la clase base, pero el valor es .T. en mi clase frmBizObjAware que se usa para formularios con reconocimiento de datos.

El método AnyChange de la clase base Formulario establece una bandera y llama a un método personalizado, UpdateEnabled, para habilitar y deshabilitar controles y elementos de menú de manera apropiada; el código de AnyChange se muestra en el Listado 11.

Listado 11. Este código en el método AnyChange del formulario responde a los cambios del usuario.

This.lDataChanged = .T.
This.UpdateEnabled(.T.)
Finalmente, en la clase base Formulario, UpdateEnabled, que se muestra en el Listado 12, solo garantiza que las condiciones Skip-For del menú se vuelvan a evaluar después de un cambio. Llama a un método abstracto que puede utilizar en formularios individuales para tratar con controles específicos que deben habilitarse o deshabilitarse.

Listado 12. Para asegurarse de que los menús y las barras de herramientas se habiliten y deshabiliten adecuadamente, la clase UpdateEnabled del formulario reactiva los menús.

LPARAMETERS lForce
* Ensure that skip for conditions get re-evaluated.
ACTIVATE MENU MainMenu NOWAIT 

IF NOT EMPTY(This.cMenuName)
 ACTIVATE MENU (This.cMenuName) NOWAIT
ENDIF

This.FormUpdateEnabled(m.lForce)

Los controles de la barra de herramientas utilizan el enlace de eventos para incorporarse al menú y determinar si deben activarse o desactivarse. El método Init de tbrBase (la clase base Barra de Herramientas) llama al método BindToActiveForm personalizado, que se muestra en el Listado 13, que vincula el método UpdateEnabled personalizado de la barra de herramientas al método UpdateEnabled del formulario de llamada. Tenga en cuenta que la llamada BindEvent incluye un valor de 1 para el parámetro nFlags, lo que indica que el método delegado (el método UpdateEnabled de la barra de herramientas) debe llamarse después de que se ejecute el método fuente (el método UpdateEneabled del formulario o del objeto de aplicación). Entonces, la barra de herramientas tiene sus controles actualizados cada vez que se actualizan el formulario y el menú, pero después.

Listado 13. Este método, BindToActiveForm, asegura que los controles de la barra de herramientas se actualicen cuando los controles de formulario y el menú lo hacen.

LPARAMETERS oForm
* Bind updating of controls to updating on active form
DO CASE
CASE VARTYPE(m.oForm) = "O" AND NOT ISNULL(m.oForm)
 BINDEVENT(m.oForm, "UpdateEnabled", This, "UpdateEnabled", 1)

CASE VARTYPE(goApp) = "O" AND MethodExists(goApp.oActiveForm, "UpdateEnabled")
 BINDEVENT(goApp.oActiveForm, "UpdateEnabled", This, "UpdateEnabled", 1)
ENDCASE

El método UpdateEnabled de la barra de herramientas se basa en los propios controles para conocer la condición para habilitar o deshabilitar. Simplemente recorre todos los controles y llama al código al que apunta el control. El método se muestra en el Listado 14.

Listado 14. El método UpdateEnabled de la barra de herramientas le dice a cada control que evalúe su propia condición Skip For.

* Update the buttons on the toolbar to reflect the current state of affairs
* Each button should have a cSkipFor expression to use for this.
* If not, leave it as is
* Use TRY-CATCH to avoid problems in special cases (such as the app is closing)
FOR EACH oControl IN THIS.Controls
 IF TYPE("oControl.cSkipFor")="C"
 TRY
 oControl.Enabled = NOT EVALUATE(oControl.cSkipFor)
 CATCH
 * Nothing to do
 ENDTRY
 ENDIF
ENDFOR

Debido a que varios formularios pueden compartir la misma barra de herramientas, no es suficiente vincular la barra de herramientas al formulario en el método Init. Necesitamos cambiar el enlace ya que el usuario activa diferentes formularios.

El objeto de la aplicación rastrea el formulario activo en la aplicación mediante otro uso de BindEvent(); se llama a un par de métodos de aplicación cuando cambia el formulario activo. El método Activate de cada formulario está vinculado al método SetActiveForm del objeto aplicación, mientras que el método Deactivate de cada formulario está vinculado al método ClearActiveForm del objeto aplicación. El método Init del formulario llama al método BindForm del objeto aplicación para configurar esto. BindForm se muestra en el Listado 15. SetActiveForm y ClearActiveForm simplemente cambian la propiedad de una aplicación para que siempre apunte al formulario activo.

Listado 15. El método BindForm del objeto aplicación permite que la aplicación realice un seguimiento de qué formulario está activo actualmente.

PROCEDURE BindForm(oForm)
* Bind a form's events as needed
BINDEVENT(oForm, "Activate", This, "SetActiveForm", 1)
BINDEVENT(oForm, "Deactivate", This, "ClearActiveForm")
IF PEMSTATUS(oForm, "GotBarCode",5)
 BINDEVENT(oForm, "GotBarCode", This, "ProcessBarCode")
ENDIF
RETURN

El método Init de tbrBase se vincula a este modelo vinculando SetActiveForm y ClearActiveForm a los métodos BindToActiveForm y UnbindActiveForm de la barra de herramientas.

Nuevamente aquí, el método delegado (BindActiveForm o UnbindActiveForm) se ejecuta después del método fuente (SetActiveForm o ClearActiveForm). El método Init de la barra de herramientas se muestra en el Listado 16, mientras que el método UnbindActiveForm está en el Listado 17.

Listado 16. El método Init de la clase base Barra de Herramientas usa BindEvent() para asegurar que los controles de la barra de herramientas se vinculen y desvinculen de manera apropiada.

LPARAMETERS oCallingForm
* Reset enable/disable of controls when active form changes
IF MethodExists(goApp, "ClearActiveForm")
 * Disconnect from old form
 BINDEVENT(goApp, "ClearActiveForm", This,"UnbindActiveForm",1)
ENDIF
IF MethodExists(goApp, "SetActiveForm")
 * Connect to new form
 BINDEVENT(goApp, "SetActiveForm", This,"BindToActiveForm",1)
ENDIF
* Bind to current form
IF VARTYPE(m.oCallingForm) = "O" AND NOT ISNULL(m.oCallingForm)
 This.BindToActiveForm(m.oCallingForm)
ENDIF

Listado 17. Este método de barra de herramientas, UnbindActiveForm, se llama cuando el formulario al que están enlazados los controles de la barra de herramientas está desactivado. Libera las vinculaciones, de modo que la barra de herramientas puede, si corresponde, vincularse a otro formulario.

* Unbind from active form.
IF VARTYPE("goApp") = "O" AND VARTYPE("goApp.oActiveForm") = "O" AND ;
 PEMSTATUS(goApp.oActiveForm, "UpdateEnabled", 5)
 UNBINDEVENTS(goApp.oActiveForm, "UpdateEnabled", This, "UpdateEnabled")
ENDIF

Toda esta secuencia probablemente parece bastante complicada, pero de hecho, proporciona un mecanismo virtualmente invisible para mantener los elementos del menú y los controles de la barra de herramientas correctamente habilitados y deshabilitados. Todo lo que se requiere al crear formularios es el código de nivel de formulario personalizado en FormUpdateEnabled. Para los menús, configurar la condición Skip-For para que funcione, mientras que para las barras de herramientas, todo lo que tiene que hacer es configurar la propiedad personalizada cSkipFor de cada control.

Cambiar el tamaño de las barras de herramientas

La adición de la propiedad Anchor en Visual FoxPro 9 hizo que fuera mucho más fácil cambiar el tamaño de los controles correctamente cuando se cambia el tamaño de un formulario. Pero hay situaciones en las que Anchor no es suficiente. En una aplicación, utilizo controles dentro de una barra de herramientas para proporcionar formularios acoplables dentro del formulario principal de la aplicación (que es un formulario de nivel superior). Las barras de herramientas no tienen una propiedad Anchor; su tamaño está controlado por el tamaño de su contenido. Cuando el usuario cambia el tamaño de la aplicación, quiero asegurarme de que la barra de herramientas cambia de tamaño; para hacerlo, tengo que cambiar el tamaño de los objetos en la barra de herramientas. BindEvent() lo hace posible. Sin embargo, si no se hace con cuidado, también puede bloquear la aplicación.

Primero construí esta funcionalidad para proporcionar una barra de estado para un formulario de nivel superior que es el formulario principal de la aplicación. Pongo el control ActiveX StatusBar dentro de una barra de herramientas. Vinculé el método Resize del formulario a un método personalizado de la barra de herramientas, llamado ResizeStatus. Tenga en cuenta que esto es, en cierto sentido, lo contrario de las vinculaciones en algunos de los ejemplos anteriores. Allí, empujamos un evento desde un control hasta su contenedor o formulario contenedor. Aquí, lo estamos empujando hacia abajo para que un control particular reaccione cuando algo le sucede al formulario que lo contiene.

Lo que dificulta esta operación es que el método Resize del formulario se activa repetidamente siempre que cambie el tamaño del formulario, en lugar de una vez cuando haya terminado. Restablecer el tamaño de la barra de estado cada vez que se activa el método Cambiar tamaño del formulario hace que la barra de herramientas cambie de tamaño repetidamente, lo que afecta al tamaño del formulario, etc. Esa secuencia finalmente bloquea VFP 9.

Entonces necesitaba una forma de controlar el cambio de tamaño y hacerlo solo una vez, cada vez que se cambia el tamaño del formulario. Subclasé la clase OLEControl y le agregué el control de la barra de estado. Esa clase, sbrMSStatus, tiene un método ResizeStatus personalizado que contiene el código del Listado 18.

Listado 18. El método ResizeStatus personalizado del control de la barra de estado establece su ancho en un valor especificado.

LPARAMETERS nNewWidth

IF VARTYPE(m.nNewWidth) = "N"
 This.Width = m.nNewWidth
ELSE
 This.Width = ThisForm.Width
ENDIF

La clase base Barra de Herramientas contiene una instancia de sbrMSStatus y tiene dos propiedades personalizadas. oForm contiene una referencia de objeto al formulario contenedor, mientras que lResizingNow es una marca para indicar si estamos en medio de un cambio de tamaño. La barra de herramientas tiene un método ResizeStatus personalizado, que contiene el código del Listado 19.

Listado 19. El método ResizeStatus de la barra de herramientas asegura que cambiemos el tamaño de la barra de estado solo una vez.

LOCAL aFired[1]
IF NOT This.lResizingNow
 This.lResizingNow = .T.
 * Need width of calling form to pass in
 AEVENTS(aFired, 0)
 This.oStatusBar.ResizeStatus(aFired[1].Width)
 This.lResizingNow = .F.
ENDIF

El método Init de la barra de herramientas, que se muestra en el Listado 20, llena la propiedad oForm, y el método BeforeDock, que se muestra en el Listado 21, asegura que la barra de estado llene todo el ancho del formulario cuando lo acople. (El código probablemente debería asegurarse de que solo lo esté acoplando arriba o abajo antes de ajustar el ancho).

Listado 20. La barra de herramientas tiene un puntero al formulario contenedor, almacenado en el método Init.

DODEFAULT()
This.oForm = _VFP.ActiveForm

Listado 21. El método BeforeDock de la barra de herramientas hace que el ancho de la barra de estado coincida con el del formulario.

LPARAMETERS nLocation
* Make sure status bar fills form width
This.oStatusBar.Width = This.oForm.Width

El formulario hace su parte del trabajo en el método Activate. Si la barra de herramientas aún no existe, se crea una instancia y el método Resize del formulario está vinculado al método ResizeStatus de la barra de herramientas. Si la barra de herramientas aún no está acoplada en la parte inferior, la acoplamos. El Listado 22 muestra el código de activación.

Listado 22. El método Activate del formulario configura la barra de herramientas de la barra de estado la primera vez que se ejecuta.

IF ISNULL(This.oStatusBar)
 This.oStatusBar = NEWOBJECT("tbrStatusBar", "ebControls")
 BINDEVENT(This, "Resize", This.oStatusBar, "ResizeStatus", 1)
 This.oStatusBar.Show()
ENDIF

IF This.oStatusBar.DockPosition <> 3
 This.oStatusBar.Dock(3)
ENDIF

Tenga en cuenta que, incluso con este enfoque, esta barra de herramientas no es compatible con el formulario principal de la aplicación Biblioteca, donde los botones de las barras de herramientas pueden tener diferentes tamaños. Ese escenario colapsa VFP.

Este es otro ejemplo en el que el método delegado se ejecuta después del método del objeto de origen.

Necesitamos hacer las cosas en ese orden para que el Ancho del formulario haya cambiado para cuando queramos pasarlo al método ResizeStatus de la barra de estado.

Hay un ejemplo de un formulario con una barra de herramientas de barra de estado en los materiales de esta sesión, pero no se usa en la aplicación Biblioteca porque esa aplicación acopla y desacopla otras barras de herramientas. Para probarlo, ejecute ebMain.PRG.

Supervisión de la actividad del usuario

En algunas aplicaciones, queremos prestar atención a si el usuario está haciendo algo. Podríamos estar ejecutando un proceso en segundo plano que debería detenerse cuando el usuario quiera hacer otra cosa, o podríamos querer cerrar la sesión del usuario después de un período de inactividad. En cualquier caso, necesitamos saber cada vez que el usuario mueve el ratón o escribe. BindEvent() nos da una forma de rastrear la actividad del usuario.

El seguimiento de la actividad del usuario es una tarea a nivel de aplicación, por lo que la clase Aplicación, cusApp, necesita varias propiedades personalizadas. Los dos primeros administran el seguimiento de la actividad: lTrackUserActivity, que indica si estamos rastreando la actividad del usuario; y oActivityTimer, una referencia a un objeto temporizador utilizado para el seguimiento. Dos propiedades adicionales nos permiten indicar qué temporizador usar para el seguimiento de la actividad: cTimerClass y cTimerClassLib apuntan a la clase Timer a usar.

El método Init de la clase Aplicación configura el temporizador, si la propiedad lTrackUserActivity es verdadera, como se muestra en el Listado 23.

Listado 23. El método Init de la clase Aplicación configura un temporizador para rastrear la actividad del usuario.

IF This.lTrackUserActivity
 This.SetupActivityTimer()
ENDIF

El método SetupActivityTimer, que se muestra en el Listado 24, simplemente crea una instancia del temporizador y almacena una referencia en la propiedad oActivityTimer. TRY-CATCH se utiliza en caso de que haya algún problema.

Listado 24. Configurar el temporizador de actividad es tan fácil como crear una instancia del objeto correcto.

LOCAL lSuccess
TRY
 This.oActivityTimer = NEWOBJECT(This.cTimerClass, This.cTimerClassLib)
 lSuccess = .T.
CATCH
 lSuccess = .F.
ENDTRY
RETURN m.lSuccess

La clase base Formulario (frmBase) tiene una propiedad personalizada, lBindUserActions, y un método personalizado, BindUserActions. En el método Init del formulario, verificamos la propiedad y, si es verdadera, llamamos al método, como se muestra en el Listado 25.

Listado 25. La propiedad lBindUserActions determina si rastrear la actividad del usuario en el formulario.

IF This.lBindUserActions
 This.BindUserActions()
ENDIF

Quizás se pregunte por qué necesitamos la propiedad lBindUserActions en el nivel de formulario cuando ya realizamos un seguimiento a nivel de aplicación. Puede haber situaciones en las que un formulario en particular deba excluirse del seguimiento de la actividad o, a la inversa, en las que solo las acciones en ciertos formularios se consideren actividad del usuario.

El método BindUserActions del formulario, que se muestra en el Listado 26, llama a un método del objeto de aplicación. La declaración IF garantiza que podamos ejecutar formularios de forma independiente para realizar pruebas (es decir, cuando no se ha creado una instancia del objeto de la aplicación). Tenga en cuenta que pasamos una referencia de objeto al formulario de llamada al método BindUserActions de la aplicación.

Listado 26. El método BindUserActions de la clase base Formulario llama a BindUserActions del objeto Aplicación para realizar el enlace.

* If we have an application-level object watching for user
* activity, bind things in this form to it.
IF TYPE("goApp.oActivityTimer") = "O"
 goApp.BindUserActions(THIS)
ENDIF

El método BindUserActions de la aplicación confirma que estamos rastreando y le pide al temporizador que realice el enlace real; se muestra en el Listado 27. Pasa la referencia al formulario.

Listado 27. El método BindUserActions de la aplicación delega la tarea real de vinculación al temporizador de actividad.

LPARAMETERS oObject
* Bind user actions in the specified object to the activity timer.
IF This.lTrackUserActivity
 * Only do this if we're tracking
 IF ISNULL(This.oActivityTimer)
 * In case we haven't already set it up, do so now
 This.SetupActivityTimer()
 ENDIF
 This.oActivityTimer.BindUserActionInObject(m.oObject)
ENDIF
RETURN

Todo el trabajo real ocurre en el objeto del temporizador; tmrUserActivity se deriva de la clase base Timer (tmrBase) y tiene una propiedad personalizada, lUserActed, que es .T. cuando ha habido actividad del usuario dentro del período de tiempo especificado y .F. cuando no lo ha hecho. El temporizador tiene dos métodos personalizados, BindUserActionInObject y UserActivity. BindUserActionInObject, que se muestra en el Listado 28 y se llama desde el método BindUserActions del objeto aplicación, vincula los eventos KeyPress y MouseMove de cada control en el objeto especificado al método UserActivity, y profundiza de forma recursiva para garantizar que no importa dónde el usuario escriba o mueva el mouse , lo atrapamos.

Listado 28. El método BindUserActionInObject del temporizador de actividad vincula los eventos KeyPress y MouseMove del objeto especificado y (recursivamente) cada control que contiene al método UserActivity del temporizador.

LPARAMETERS oObject
IF PEMSTATUS(oObject, "KeyPress", 5)
 BINDEVENT(oObject, "KeyPress", This, "UserActivity")
ENDIF
IF PEMSTATUS(oObject, "MouseMove", 5)
 BINDEVENT(oObject, "MouseMove", This, "UserActivity")
ENDIF
IF PEMSTATUS(oObject, "Objects", 5)
 FOR EACH oChild IN oObject.Objects
 This.BindUserActionInObject(m.oChild)
 ENDFOR
ENDIF
RETURN

Por lo tanto, cualquier acción del usuario en el formulario activa el método UserActivity del temporizador.

La clase tmrUserActivity simplemente administra el indicador lUserActed; necesita ser mejorado o subclasificado para hacer algo basado en la actividad o inactividad del usuario. Aquí, el método UserActivity establece la bandera y restablece el temporizador, de modo que comienza a observar la inactividad nuevamente. El método se muestra en el Listado 29.

Listado 29. El método UserActivity se activa cada vez que el usuario escribe o mueve el mouse.

LPARAMETERS uParm1, uParm2, uParm3, uParm4
* Parameters for bound events
This.lUserActed = .T.
* So start counting again.
This.Reset()
RETURN

Vale la pena mencionar los parámetros de UserActivity. Cuando un método es un delegado de un evento, debe aceptar los mismos parámetros que el método fuente. Dado que MouseMove acepta cuatro parámetros, necesitamos cuatro parámetros, aunque no haremos nada con ellos. (KeyPress acepta dos parámetros, pero aceptar cuatro aquí no causará ningún problema). El método Timer del temporizador borra el indicador lUserActed porque cuando se activa, indica que el tiempo especificado ha pasado sin actividad del usuario.

Para facilitar la escritura de código para responder a la actividad o inactividad del usuario, la propiedad personalizada lUserActed tiene un método Assign. (Consulte la siguiente sección principal de este documento para obtener detalles sobre cómo funcionan los métodos Assign). Ese método se activa cada vez que cambiamos lUserActed, ya sea desde UserActivity o Timer. En una subclase, podemos poner código en ese método para tomar la acción apropiada según el nuevo valor.

En la aplicación para la que creé originalmente el temporizador de actividad, el objetivo era comenzar una actividad en segundo plano (sondeo de cambios en una pieza de hardware dedicado) después de que el usuario estuvo inactivo durante 10 segundos, y detenerla tan pronto como el usuario se activara otra vez.

Un ejemplo más simple, incluido en la aplicación Biblioteca, es preguntar al usuario si debe cerrar la aplicación después de un período de inactividad. Para crear este temporizador, subclasé tmrUserActivity para crear tmrShutDown. La propiedad Interval se establece en 60000, que es 1 minuto (60.000 milisegundos). En una aplicación real, probablemente establecería el intervalo mucho más alto; un minuto de inactividad es terriblemente corto para cerrar una aplicación. El método lUserActed_Assign, que se muestra en el Listado 30, verifica si el nuevo valor de la propiedad es .T. o .F. Si es .F., Se le pide al usuario que indique si debe cerrar la aplicación. Dado que un período de inactividad puede significar que el usuario ya no está sentado frente a la computadora, el InputBox() utilizado para indicarle al usuario tiene un tiempo de espera de un minuto y un tiempo de espera predeterminado de "Y" para cerrar la aplicación.

Listado 30. Este código, en el método lUserActed_Assign de la subclase del temporizador, pregunta al usuario si debe cerrar la aplicación. Si el usuario dice que sí o no responde en un minuto, la aplicación se cierra.

LPARAMETERS tuNewValue
LOCAL cResponse
* Shut down the application, if inactivity and user agrees
IF NOT m.tuNewValue
 cResponse = INPUTBOX("You don't seem to be doing anything. " + ;
 "Do you want to shut down this application (Y/N)?", ;
"No activity", "Y", 60000, "Y", "N")
 IF UPPER(m.cResponse) = "Y"
 IF TYPE("goApp") = "O" AND NOT ISNULL(m.goApp)
 goApp.ShutDownApp()
 ENDIF
 ENDIF
ENDIF
This.lUserActed = m.tuNewValue

El método ShutDownApp del objeto de la aplicación, como su nombre indica, cierra la aplicación de forma ordenada. La aplicación de la biblioteca utiliza esta configuración y permite al usuario decidir (a través del formulario de Preferencias) si cerrar después de un período de inactividad y cuánto tiempo debe ser ese período.

Continua en: "Enlazar eventos para obtener mejores aplicaciones - Parte 3 de 4" ...


Copyright 2017, Tamar E. Granor

No hay comentarios. :

Publicar un comentario

Los comentarios son moderados, por lo que pueden demorar varias horas para su publicación.