24 de marzo de 2021

Enlazar eventos para obtener mejores aplicaciones - Parte 4 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 3 de 4"

El uso del método Assign

Encontré más formas de usar métodos Assign que métodos Access. Aunque un método Assign le permite cambiar el valor asignado, rara vez uso esa capacidad. Más a menudo, mi código de método Assign me permite asegurarme de que sucedan cosas adicionales cuando se guarda el valor.

A menudo, el objetivo es la encapsulación; Al usar un método Assign, evito que el código que cambia una propiedad tenga que saber a qué afecta un cambio en esa propiedad.

Actualizar una marca de tiempo

En la aplicación de la que se extrae la Figura 7, hacemos un seguimiento del valor actual (el "Valor real") de cada uno de los elementos que se muestran en el formulario. Estos valores se leen del hardware real y queremos saber no solo el valor, sino también cuándo se leyó (entre otras cosas, para mostrar la última fecha de lectura en el formulario, como en la Figura 7). Un objeto comercial contiene la información de un solo artículo; hay una colección de tales artículos. El objeto comercial tiene una propiedad cActual para el valor actual y una propiedad tLastRead para la marca de tiempo. Un método Assign para actualizaciones cActual tLastRead, como en el Listado 45.

Listado 45. El método Assign para una propiedad que rastrea los valores leídos desde el hardware actualiza la marca de tiempo del valor.

LPARAMETERS tuNewValue
IF NOT (ALLTRIM(THIS.cActual) == ALLTRIM(m.tuNewValue))
 THIS.cActual = m.tuNewValue
 * Set last read time.
 THIS.tLastRead = DATETIME()
ENDIF

Establecer una bandera "sucia" (“dirty” flag)

El código del Listado 45 es en realidad solo una parte de lo que hacemos cuando leemos un nuevo valor de hardware. Necesitamos una forma de saber si los datos han cambiado desde la última vez que se almacenaron, es decir, una bandera "sucia". Los métodos Assign también proporcionan eso. Tenemos una propiedad de aplicación, lIsDirty. Siempre que guardamos datos o abrimos un nuevo archivo de datos, borramos esa propiedad. El método Assign (del Listado 45) incluye la línea adicional de código que se muestra en el Listado 46 dentro del IF; utilizamos el mismo código en los métodos Asignar de todas las propiedades donde un cambio indica que los datos ahora son diferentes de lo que eran en la última vez que se guardaron.

Listado 46. Una línea de código en un método Assign (más un pequeño código a nivel de aplicación) le permite realizar un seguimiento de si los datos han cambiado desde la última vez que se guardaron.

goApp.lIsDirty = .T.

En la aplicación de la biblioteca, ya tenemos una gran parte del marco para mantener una bandera "sucia" en toda la aplicación. Cada formulario tiene la propiedad lDataChanged. Podemos usar esas propiedades para determinar si hay datos sin guardar en la aplicación. Para hacerlo, agregamos un método Assign a lDataChanged en frmBase. Ese método llama a un método de objeto de aplicación para actualizar la bandera "sucia" de toda la aplicación.

Solo para demostrar otra posibilidad, lDataChanged_Assign también actualiza el título del formulario para que siempre que haya datos no guardados, incluya un asterisco, tal como lo hace VFP. El método se muestra en el Listado 47.

Listado 47. Este código en el método Assign de la propiedad lDataChanged del formulario ayuda a mantener un indicador "sucio" en toda la aplicación y tiene el título de cada formulario que indica si actualmente tiene cambios sin guardar.

LPARAMETERS tuNewValue
This.lDataChanged = tuNewValue
* Set app-level dirty flag
IF VARTYPE("goApp") = "O" AND NOT ISNULL(goApp) AND ;
 PEMSTATUS(goApp, "SetDirtyFlag", 5)
 goApp.SetDirtyFlag(m.tuNewValue)
ENDIF
* Update form caption
IF m.tuNewValue
 IF RIGHT(This.Caption, 1) <> "*"
 This.Caption = This.Caption + " *"
 ENDIF
ELSE
 IF RIGHT(This.Caption, 1) = "*"
 This.Caption = LEFT(This.Caption, LEN(This.Caption)-2)
 ENDIF
ENDIF

El método SetDirtyFlag del objeto de aplicación, junto con una propiedad del objeto de aplicación, lUnsavedData, hace el resto del trabajo. SetDirtyFlag, que se muestra en el Listado 48, verifica el valor que se le pasa; si es .T., algún formulario tiene datos sin guardar, por lo que la bandera se establece en .T. Si el parámetro es .F., Sabemos que al menos un formulario acaba de guardar datos o restaurar los datos anteriores, pero no conocemos el estado de los otros formularios, por lo que los recorremos hasta que encontramos uno sin guardar. datos.

Listado 48. Este método gestiona una bandera "sucia" en toda la aplicación, de modo que podamos saber con sólo comprobar una propiedad si hay datos no guardados.

PROCEDURE SetDirtyFlag(lNewValue)
* Set the application dirty flag. If the parameter
* is true, then some form has changed data, and we can
* just set this flag. If the parameter is false, some
* form has just saved or reverted its changed data
* and we need to look at all open forms.
IF m.lNewValue
 This.lUnsavedData = .T.
ELSE
 This.lUnsavedData = .F.
 FOR EACH oForm IN _VFP.Forms FOXOBJECT
 IF PEMSTATUS(oForm, "lDataChanged", 5)
 IF oForm.lDataChanged
 This.lUnsavedData = .T.
 * No need to find more than one
 EXIT
 ENDIF
 ENDIF
 ENDFOR
ENDIF
RETURN

En la mayoría de las aplicaciones de ingreso de datos, el siguiente paso sería marcar la bandera "sucia" al salir y pedirle al usuario que guarde los cambios si hay datos no guardados. La aplicación Biblioteca se diseñó para que cuando cierre un formulario, los datos se guarden automáticamente, por lo que no es necesario preguntar.

Delegar el manejo de un nuevo valor

Para una aplicación, necesitaba la capacidad de administrar un conjunto complejo de preferencias de usuario. Los elementos de preferencia se correlacionan más o menos directamente con las propiedades del objeto de la aplicación o con las propiedades de los objetos gestionados por el objeto de la aplicación. Por ejemplo, una preferencia aborda la frecuencia con la que se realiza la recolección de basura; esta propiedad debe convertirse en el intervalo de un temporizador. Al crear una propiedad de objeto de aplicación para cada preferencia y dándoles métodos Assign, me aseguré de que las actualizaciones apropiadas ocurrieran automáticamente.

La aplicación Biblioteca tiene sólo un par de elementos en su formulario de Preferencias, que se muestra en la Figura 10. Utilice mapas de barras de herramientas grandes directamente a una propiedad de la aplicación; utiliza BindEvent() para activar el cambio de tamaño de los botones en la barra de herramientas. (Tenga en cuenta que este código es incompatible con el código de cambio de tamaño de la barra de herramientas descrito anteriormente en este documento).

La casilla de verificación que controla si la aplicación se apaga después de un período específico de inactividad también se asigna a una propiedad de la aplicación. Sin embargo, el temporizador que rastrea la inactividad (descrito en "Monitoreo de la actividad del usuario" anteriormente en este documento) debe habilitarse o deshabilitarse de manera apropiada cuando el usuario cambia la casilla de verificación. De manera similar, la ruleta que determina cuánto tiempo esperar para la actividad necesita establecer el intervalo para ese temporizador. (Nada de esto sucede hasta que el usuario cierra el formulario de Preferencias). Ambos elementos utilizan métodos Assign para garantizar que se produzcan los cambios adecuados.

Figura 10. El formulario de preferencias de la aplicación de la biblioteca se basa en los métodos Assign para garantizar que el temporizador de inactividad se establezca correctamente.

Cuando el usuario cierra el formulario, los valores de control se almacenan en las propiedades de la aplicación correspondiente. El valor de la ruleta se multiplica por 60000, la cantidad de milisegundos en un minuto, primero. El valor de ruleta ajustado se almacena en una propiedad de la aplicación llamada nActivityTimerInterval. El método nActivityTimerInterval_Assign establece el intervalo del temporizador, como se muestra en el Listado 49. Tenga en cuenta que necesitamos almacenar el valor en la propiedad de la aplicación, así como establecer el intervalo del temporizador. De lo contrario, la próxima vez que abramos el cuadro de diálogo Preferencias, no configurará la ruleta en el valor actual.

Listado 49. Este método Assign establece el intervalo para el temporizador de actividad cuando el usuario cambia la configuración en Preferencias.

PROCEDURE nActivityTimerInterval_Assign(nNewValue)
* Something changed interval for activity timer. Propagate to timer
IF VARTYPE(m.nNewValue) = "N" AND m.nNewValue > 0 AND ;
 m.nNewValue <> This.oActivityTimer.Interval
 This.nActivityTimerInterval = m.nNewValue
 This.oActivityTimer.Interval = m.nNewValue
ENDIF
RETURN

El valor de la casilla de verificación se almacena en la propiedad lTrackUserActivity. Su método Assign habilita o deshabilita el temporizador, como se muestra en el Listado 50.

Listado 50. Cuando el usuario cambia la preferencia para rastrear la inactividad del usuario, se activa el método Assign de lTrackUserActivity.

PROCEDURE lTrackUserActivity_Assign(lNewValue)
* Tracking decision changed. Set up or disable timer
IF VARTYPE(m.lNewValue) = "L" AND m.lNewValue <> This.lTrackUserActivity

 This.lTrackUserActivity = m.lNewValue
 IF This.lTrackUserActivity
 This.SetupActivityTimer()
 ELSE
 This.DisableActivityTimer()
 ENDIF
ENDIF
RETURN

Compruebe la validez

Una de las cosas que puede hacer con un método Assign es comprobar la validez del nuevo valor de una propiedad y rechazar los valores no válidos. Por ejemplo, escribí un juego de Sudoku en VFP hace unos años (como demostración de objetos comerciales; se describe en http://tinyurl.com/y84fj5p3). El objeto bizGame, que representa el juego en su conjunto, tiene una propiedad llamada nSize que contiene el tamaño del tablero (el número de celdas en cualquier dirección). Dado que Sudoku requiere que el tamaño del juego sea un cuadrado perfecto, nSize tiene un método Assign que verifica; se muestra en el Listado 51.

Listado 51. El método nSize_Assign del objeto bizGame de una aplicación de Sudoku asegura que el tamaño de juego especificado sea válido.

* Ensure that size is a perfect square
LPARAMETERS tuNewValue
IF SQRT(m.tuNewValue) = INT(SQRT(m.tuNewValue))
 This.nSize = tuNewValue
ENDIF
RETURN

Propagar datos dentro de un contenedor

Una de las principales formas en que uso los métodos Assign es empujar los datos hacia abajo en una jerarquía dentro de un contenedor, de modo que solo el contenedor esté expuesto al mundo. Hay una variedad de comportamientos en este sentido que son útiles.

Anteriormente en este documento, mostré cómo usar los métodos de Access para proporcionar ToolTips dinámicos y tener el mismo ToolTip para un contenedor y su contenido. Assign proporciona una forma alternativa de hacer esto último, en situaciones en las que no necesita sugerencias dinámicas. Es decir, este enfoque funciona cuando desea asignar un ToolTip fijo a un contenedor y hacer que todos los controles internos muestren la misma sugerencia.

Mi código para esto usa una propiedad lógica personalizada, lPropagateTooltipsDown, en la clase base Contenedor. El método ToolTipText_Assign contiene el código del Listado 52. Si el contenedor tiene el indicador establecido en .T., Cuando cambia el ToolTip, recorremos los objetos en el contenedor y cambiamos ToolTipText para cada uno de ellos.

Listado 52. Este código en el método ToolTipText_Assign le permite propagar el ToolTipText de un contenedor a los objetos contenidos.

LPARAMETERS tuNewValue
This.ToolTipText = tuNewValue
LOCAL oObject
IF THIS.lPropagateToolTipDown
 FOR EACH oObject IN THIS.OBJECTS FOXOBJECT
 IF PEMSTATUS(m.oObject, "ToolTipText", 5)
 m.oObject.ToolTipText = m.tuNewValue && don't use ToolTipText on the right
 && in case it also has an access method
 ENDIF
 ENDF
ENDIF

Tenga en cuenta que no tenemos que hacer esto de forma recursiva, incluso si el contenedor contiene otros} contenedores. El método ToolTipText_Assign para los objetos contenidos se activará y, siempre que tengan lPropogateToolTipDown establecido en .T., Manejará sus objetos contenidos.

Ajustar una etiqueta

En una aplicación, necesitaba tener una etiqueta que se ejecutara verticalmente dentro de una forma. En la forma correspondiente (que se muestra en la Figura 11), los títulos de la etiqueta se determinan dinámicamente en función de los datos. Creé una subclase de etiqueta y le di a Caption un método Assign. Ese método llama a un método TurnCaption personalizado para reconstruir el título y agregar la puntuación adecuada para que se muestre verticalmente. Para las formas de doble ancho en la fila superior, el mismo método divide el título en dos cadenas de aproximadamente la misma longitud y crea la cadena de subtítulos adecuada. TurnCaption se muestra en el Listado 53. (Tenga en cuenta que CHR (160) es un espacio sin interrupciones).

Figura 11. Las etiquetas verticales dentro de los cuadros aquí se configuran mediante un método Assign.

Listado 53. Este código es llamado por el método Caption_Assign de las etiquetas verticales que se muestran en la Figura 11.

LPARAMETERS cCaption, lTwoColumns
#DEFINE LF CHR(10)
LOCAL cTurnedCaption, nLength, nChar
m.cCaption = CHRTRAN(CHRTRAN(m.cCaption, CHR(160), " "), LF, "")
nLength = LEN(m.cCaption)
IF m.lTwoColumns
 * Find a dividing point
 nWords = GETWORDCOUNT(m.cCaption)
 IF m.nWords > 1
 * Break on a word break
 cColumn1 = GETWORDNUM(m.cCaption, 1)
 cColumn2 = ALLTRIM(SUBSTR(m.cCaption, LEN(m.cColumn1) + 1))
 cNextWord = GETWORDNUM(m.cColumn2, 1)
 nWord = 2
 DO WHILE m.nWord <= m.nWords-1 AND ;
 LEN(m.cColumn1) + LEN(m.cNextWord) > LEN(m.cColumn2) - LEN(m.cNextWord)
 cColumn1 = m.cColumn1 + " " + m.cNextWord
 cColumn2 = ALLTRIM(SUBSTR(m.cColumn2, LEN(m.cNextWord) + 1))
 cNextWord = GETWORDNUM(m.cColumn2, 1)
 nWord = nWord + 1
 ENDDO

 nLength = MAX(LEN(m.cColumn1), LEN(m.cColumn2))
 ELSE
 * Just break it in half
 cColumn1 = LEFT(m.cCaption, FLOOR(m.nLength/2))
 cColumn2 = RIGHT(m.cCaption, CEILING(m.nLength/2))

 * Pad shorter string
 cColumn1 = PADR(m.cColumn1, LEN(m.cColumn2))
 ENDIF
ELSE
 cColumn1 = m.cCaption
 cColumn2 = ""
ENDIF
cTurnedCaption = ""
FOR nChar = 1 TO nLength
 cTurnedCaption = m.cTurnedCaption + EVL(SUBSTR(m.cColumn1, nChar, 1),CHR(160))
 IF m.lTwoColumns
 cTurnedCaption = m.cTurnedCaption + CHR(160) + CHR(160) + ;
 SUBSTR(m.cColumn2, nChar, 1)
 ENDIF
 cTurnedCaption = m.cTurnedCaption + LF
ENDFOR
RETURN m.cTurnedCaption

Para la aplicación Biblioteca, puede usar este código si desea mostrar libros de forma gráfica con títulos en el lomo.

Depurar con BindEvent(), Access y Assign

Como puede imaginar, tener eventos enlazados y métodos Access y Assign hace que la depuración sea más complicada. A veces, un método parece agotarse de la nada, sin señales de que se esté llamando.

No existen fórmulas mágicas para facilitar este tipo de depuración. Sin embargo, las mismas buenas prácticas de depuración que funcionan para el código VFP generalmente son útiles aquí. En particular, considero que el comando DEBUGOUT, los puntos de interrupción y el rastreo son las herramientas más efectivas.

Cuando la secuencia de eventos no parezca tener sentido para usted, distribuya DEBUGOUT PROGRAM() a través de su código, colocándolo en cada método que parezca involucrado. Luego, cuando ejecute el código con la ventana de salida de depuración disponible, verá la secuencia de métodos llamados. (Mejor aún, no tiene que eliminar estas declaraciones antes de distribuir su aplicación. DEBUGOUT se ignora en el entorno de ejecución). Una vez que haya reducido las cosas hechas a una sección particular de código, establezca un punto de interrupción para detener la ejecución al principio de esa sección y recorra el código una línea a la vez para ver exactamente qué está sucediendo.

En algunos casos, he descubierto que ni siquiera el rastreo me resuelve el misterio, o que rastrear el código interfiere con cualquier proceso que esté viendo. En tales casos, utilizo la herramienta Registro de cobertura. Enciendo el inicio de sesión en el momento en que creo que comienza el problema, luego ejecuto el código hasta que haya ejecutado la sección del problema. Luego utilizo el registro de cobertura generado para mostrarme exactamente qué líneas de código se ejecutaron en qué orden.

La otra herramienta que es útil al depurar problemas de BindEvent() es la función AEvents(). Puedo usarlo en cualquier punto de interrupción para ver exactamente lo que está vinculado, así como para ver si un evento vinculado me llevó a este punto.

Para obtener más consejos de depuración generales, consulte mi artículo en http://tinyurl.com/ycqrjufe.

Configúrelo y olvídelo

Mientras escribía este artículo, me sorprendió la cantidad de formas que encontré para usar BindEvent(), Access y Assign. Mirando los ejemplos aquí, uno de los temas principales es "configúrelo y olvídelo". Es decir, a menudo lo que estas características le permiten hacer es configurar un comportamiento deseable en sus clases base y luego usarlo una y otra vez sin tener que recordar cómo lo hizo. En algunos casos, debe establecer una propiedad o dos, pero en general, las técnicas de este artículo reducen la cantidad de código que debe escribir. ¿No es ese el objetivo de la Programación Orientada a Objetos?







Enlace a todas las partes de este artículo:

Enlazar eventos para obtener mejores aplicaciones: Parte 1 de 4 - Parte 2 de 4 - Parte 3 de 4 - Parte 4 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.