28 de diciembre de 2017

Buffering de Datos y Multiusuarios (Parte III)

Autor: Doug Hennig
Traducido por: Germán Giraldo G.


Escribir a una Tabla Buffered
Como vimos antes, tableupdate(.T.) intenta escribir todos los registros de un buffer de tabla al disco. Al igual que la versión buffer de registro, devolverá .F. si no puede actualizar un registro a causa de que otro usuario lo ha cambiado (entre otras razones). La rutina que vimos antes para atrapar errores trabaja bien para buffering de fila, debido a que nos preocupamos por un único registro a la vez. Sin embargo, con buffering de tabla, tenemos que mirar cada registro uno por uno. Debido que podríamos tener en el buffer una mezcla de registros modificados y no modificados, cómo sabemos cuáles registros se actualizarán? Para hacerlo mas complicado, si tableupdate(.T.) falla, no sabemos cuáles registros fallaron; algunos registros pueden haberse guardado y allí podría haber mas de que un registro en conflicto. La nueva función getnextmodified() nos informará exactamente lo que necesitamos saber: el número de registro para el siguiente registro modificado. Si devuelve 0, no hay mas registros modificados en el buffer. Esta función acepta dos parámetros: el primero es el número de registro después del cual buscará los siguientes registros modificados, y el segundo es el alias o área de trabajo dónde buscar. Inicialmente, usted debería pasar 0 como el primer parámetro así getnextmodified() encuentra el primer registro modificado. Para hallar el siguiente, pase el número de registro del registro actual. Aquí está un ejemplo de la anterior rutina para el manejo de conflictos, modificada para manejar los cambios en una tabla buffered cuando tableupdate(.T.) falla:

* Hallar el primer registro modificado, entonces procesar cada uno.
lnChanged = getnextmodified(0)
do while lnChanged <> 0
    * Mover al registro y tratar de bloquearlo.
    go lnChanged
    if rlock()
    * Verificar cada campo para ver cuál tiene un conflicto.
       llConflict = .F.
       for lnI = 1 to fcount()
          lcField      = field(lnI)
          llOtherUser  = oldval(lcField)   <> curval(lcField)
          llThisUser   = evaluate(lcField) <> oldval(lcField)
          llSameChange = evaluate(lcField) == curval(lcField)
          do case
             * Otro usuario ha editado este campo pero este usuario,
             * no ha grabado los nuevos valores.
          case llOtherUser and not llThisUser
             replace (lcField) with curval(lcField)
             * Otro usuario no ha editado este campo, o ambos han hecho
             * el mismo cambio, así no necesitamos hacer algo.
           case not llOtherUser or llSameChange
             * Uh-oh, ambos usuarios han cambiado este campo, pero a diferentes valores.
           otherwise
              llConflict = .T.
           endcase
        next lnI
      * Tenemos un conflicto, manejarlo. Si no, a diferencia del caso de buffering
      * de fila, no hacemos algo ahora debido a que todos los registros se
      * escribirán mas tarde.
      if llonflict
        lnChoice = messagebox('Otro usuario también cambió este ' + ;
                   'registro ' + ltrim(str(lnChanged)) + '. Desea sobre-escribir ' +;
                   'sus cambios (Si), no sobre-escribir pero ver los cambios (No),'+;
                   ' o cancelar sus cambios (Cancelar)?', 3 + 16, ;
                   'Problema Guardando Registro!')
        do case
           * Sobre-escribir los cambios: actualmente no necesitamos hacer algo por que
           * lo haremos todo mas tarde (este case está aquí sólo para aclarar).
           case lnChoice = 6
                 * Ver los cambios: traer otra instancia del formulario.
           case lnChoice = 7
               do form MYFORM name oName
           * Cancelar los cambios en este registro solamente.
           otherwise
               = tablerevert()
               unlock record lnChanged
           endcase
        endif llConflict
        * No podemos bloquear el registro, entonces cancelamos los cambios sólo para
        * para este registro.
     else
        = messagebox("Lo siento, no puedo guardar el registro #" + ;
                    ltrim(str(lnChanged)))
        = tablerevert()
        unlock record lnChanged
     endif rlock()
     * Hallar el siguiente registro modificado t procesarlo.
     lnChanged = getnextmodified(lnChanged)
enddo while lnChanged <> 0

* Debido a que revertimos algunos cambios donde hallamos un conflicto
* y el usuario desea cancelar sus propios cambios, se permite forzar el resto
* de las actualizaciones.

= tableupdate(.T., .T.)

Si una tabla ha cambiado en un buffer de tabla que no se ha escrito a disco y usted intenta cerrar la tabla o cambiar el modo buffering, obtendrá un error (#1545): “Buffer de tabla para alias <Alias> contiene cambios no confirmados”.

Transacciones

Como hemos visto, el buffering de tabla es una forma conveniente para hacer buffer de un número de cambios a una tabla y entonces escribir o abandonar todos los cambios al mismo tiempo. Sin embargo, hay un defecto en esta estrategia —qué sucede si uno de los registros está bloqueado o ha sido editado por otro usuario? En este caso, tableupdate(.T.) devolverá .F. y se llama a la rutina de errores. El problema: algunos registros fueron guardados y algunos no. Ahora usted tiene una confusión bastante complicada en sus manos si necesita deshacer lo cambios que se hicieron. Aquí está un ejemplo de tal problema: usted va al banco para transferir dinero desde su cuenta de ahorros a su cuenta de cheques. El programa actualiza su cuenta de ahorros reduciendo el saldo en la cantidad apropiada, entonces trata de incrementar el saldo de su cuenta de cheques por la misma cantidad. El programa podría ver algo como esto:
      seek M.ACCOUNT1
      replace BALANCE with BALANCE - M.AMOUNT
      seek M.ACCOUNT2
      replace BALANCE with BALANCE + M.AMOUNT
      llSuccess = tableupdate(.T.)
      if not llSuccess
         do ERROR_ROUTINE
       endif not llSuccess
 
Entretanto, un programa de limpieza automático ha estado procesando su cuenta de cheques, y ha reducido su saldo por la cantidad de varios cheques. El programa detecta el conflicto y decide abandonar la actualización usando tablerevert(.T.). Sin embargo, debido a que la cuanta de ahorros se actualizó con éxito, sus cambios no están el buffer, y por tanto los cambios permanecen. Ahora el banco tiene una situación de “sobregiro” que dificultará el rastreo, y un cliente muy disgustado cuando obtiene su extracto bancario al final del mes. Afortunadamente, VFP provee un mecanismo que puede resolver este problema: la transacción. Una transacción es un grupo específico de cambios que deben hacerse a la vez o abandonarse todos. Una transacción arranca con el comando begin transaction. Cualquier cambio de una tabla después de usar este comando, aún aquellos hechos con tableupdate(), no se escribirá en disco hasta que se encuentre un comando end transaction. Piense en una transacción como un “buffer de buffer”. La transacción se mantiene hasta que usted determine que todos los cambios pueden hacerse con éxito e invoque un end transaction. Si el programa falla o se reinicia el computador antes de encontrar end transaction, o si su programa llama un comando rollback por que uno de los cambios no puede hacerse con éxito, ninguno de los cambios se escribe en el disco. Miremos el ejemplo de la actualización del banco pero esta vez usemos una transacción como una cubierta para la actualización:

        begin transaction
        seek M.ACCOUNT1
        replace BALANCE with BALANCE - M.AMOUNT
        seek M.ACCOUNT2
        replace BALANCE with BALANCE + M.AMOUNT
        llSuccess = tableupdate(.T.)
        if llSuccess
            end transaction
        else
            rollback
        endif llSuccess

Si el saldo de la primer se cambia pero el de la segunda no se puede cambiar con éxito, llSuccess será .F., y el comando rollback evitará que los primeros cambios se escriban en el disco. Si todo está bien, end transaction escribirá ambos cambios a la vez. Aquí hay algunos otros conceptos relativos a transacciones:
  1. Las transacciones sólo pueden usarse con tablas unidas a bases de datos; para tablas libres no aplica.
  2. Las transacciones aplican para archivos memo (FPT) e índices (CDX) como para los DBF.

  3. Los comandos y funciones que alteran la base de datos, la tabla, o los índices de la tabla no pueden usarse durante una transacción. Por ejemplo, usar alter table ,delete tag, index on,tablerevert(),o close databases durante una transacción generará un error. Vea la documentación de VFP para una lista completa de comandos restringidos .
  4. Puede anidar transacciones hasta cinco niveles de profundidad (“Las buenas noticias: usted ya no tiene sólo cinco niveles de read. Las malas noticias: ...” <g>). Cuando se completa un nivel interior de transacción, sus cambios se agregan al caché de cambios para el siguiente nivel de transacción en lugar de escribirse en el disco. Sólo cuando se alcanza el end transaction final se escriben todos los cambios en disco. Usted puede usar la función txnlevel() para determinar el nivel de la transacción actual.

  5. A diferencia de otras construcciones estructuradas de VFP (tal como for/next o scan/endscan), begin transaction, end transaction, y rollback no tienen que estar localizado en el mismo programa. Usted podría, por ejemplo, tener una rutina común para arrancar transacciones y otra para terminarlas. Las transacciones deben mantenerse tan cortas como sea posible, sin embargo, debido a que cualquier registro se puede actualizar durante una transacción no están disponibles para otros usuarios. Ni siquiera para sólo lectura.

  6. Los registros bloqueados automáticamente por VFP durante una transacción se desbloquean automáticamente cuando se completa la transacción. Algunos bloqueo que haga manualmente no se desbloquean automáticamente; usted es responsable por desbloquear aquellos registros usted mismo. Si usted usa unlock durante una transacción, el registro actual permanece bloqueado hasta que la transacción sea hela, en ese momento todos los registros especificados se desbloquean.
  7. Aunque las transacciones le dan tanta protección como ellas pueden, aún es posible una falla de hardware o una falla del servidor durante la escritura al disco después de end transaction lo que puede causar pérdida de datos.
  8. Las transacciones sólo aplican para tablas locales. Las transacciones para tablas remotas se controlan usando los comandos sqlsetprop(), sqlcommit(), y sqlrollback(). Procesar transacciones con tablas remotas está mas allá del alcance de esta sesión.
Aquí está otra mirada a la rutina “Guardar” y la rutina de manejo de errores (en la rutina de error, el código en el ciclo do while no se muestra debido a que es la misma de la versión previa):
begin transaction
   if tableupdate(.T.)
      end transaction
   else
      rollback
      do ERROR_ROUTINE
   endif tableupdate(.T.)

procedure ERROR_ROUTINE
* Haga aquí la instalación completa, incluyendo verificar qué sucedió.
* Si hallamos un error #1585, hacemos el siguiente código.
    lnChanged = getnextmodified(0)
    do while lnChanged <> 0
       ...
    enddo while lnChanged <> 0
* Debido a que revertimos algunos cambios donde se hallaron conflictos y
* el usuario deseaba cancelar sus propios cambios, se permite forzar la
* la actualización del resto y entonces desbloquea todos los registros que
* que hemos bloqueado manualmente.
     begin transaction
     if tableupdate(.T., .T.)
        end transaction
        * Ahora han ocurrido algunos otros errores, entonces rollback los cambios
        * y muestra un mensaje de error apropiado (también podría tratar de manejarlos
        * aquí si lo desea).
     else
        = aerror(laError)
        rollback
        = messagebox('Error #' + ltrim(str(laError[1])) + ': ' + laError[2] + ;
                ' occurred while saving.')
     endif tableupdate(.T., .T.)



Otros Asusntos

Como vimos antes, getfldstate() puede usarse para determinar si ha cambiado algo en el registro actual. Esto le permite crear formularios que no necesiten un modo “Editar”; los datos en el formulario siempre están disponibles para editar. Cada evento InteractiveChange de los campos podría habilitar los botones “Guardar” y “Cancelar” sólo si el usuario ha cambiado algo, usando un código similar a:
if getfldstate(-1) = replicate('1', fcount() + 1)
  * deshabilita los botones, debido a que no ha cambiado nada
else
  * habilita los botones
endif getfldstate(-1) = replicate('1', fcount() + 1)

Para crear un formulario sin un modo editar, usted también necesita tener código que maneje el caso cuando el usuario cierra la ventan, sale de la aplicación, o se mueve a otro registro (si usa buffering de fila). El evento QueryUnload puede ayudar con las primeras dos, este evento ocurre cuando el usuario hace clic en el botón cerrar del formulario o sale de la aplicación. Podría poner código en este evento que guarde los registros antes de cerrar el formulario. En el caso de mover el puntero del registro, usted ha modificado sus rutinas de navegación de registros (primero, último, siguiente, anterior, buscar, etc.) para verificar si algún campo ha cambiado (y si es así, guarda el registro) antes de mover el puntero de registro. Usted probablemente tendría un método común en el formulario que hace todas estas verificaciones y guardar, y guardarlo donde sea necesario. Un asunto relacionado con esto es que getfldstate() podría indicar erróneamente que nada ha cambiado cuando en efecto el usuario ha cambiado el valor en el campo. Esto puede suceder si usted provee un item de menú o un botón de una barra de herramientas para guardar el registro o mover el puntero del registro. VFP sólo copia el valor en un control (tal como un Textbox) a el buffer del registro cuando el control pierde el foco. Si el usuario cambia el valor en un campo y entonces hace clic en el botón Siguiente en la barra de herramientas, el Textbox no pierde el foco (debido a que la barra de herramientas nunca recibe el foco), entonces el nuevo valor no se copia al buffer del registro y VFP no sabe que los datos han cambiado. La solución a este problema es forzar a que el valor del control actual se copie al buffer del registro antes de usar getfldstate() usando un código similar al siguiente en el evento clic del botón en la barra de herramientas:
with _screen.ActiveForm.ActiveControl
   if type('.ControlSource') <> 'U' and not empty(.ControlSource) and ;
          not evaluate(.ControlSource) == .Value
          replace (.ControlSource) with .Value
   endif type('.ControlSource') <> 'U' ...
endwith

23 de diciembre de 2017

Buffering de Datos y Multiusuarios (Parte II)

Autor: Doug Hennig
Traducido por: Germán Giraldo G.


Manejo de Errores

Continuando con el ejemplo de “Bill y Mary”, el código ejecutado cuando Bill hace clic en el botón “Guardar” usa la función tableupdate() para tratar de escribir el buffer al registro. Recuerde que Mary editó el registro y guardó sus cambios mientras Bill estaba editando el mismo registro. Cuando Bill hace clic en “Guardar”, tableupdate() devolverá .F., significando que el no escribió el buffer. Por qué? VFP no escribirá el buffer al registro bajo las siguientes condiciones:

  1. Otro usuario haya cambiado y guardado el registro mientras este usuario lo estaba editando (como sucedió en el ejemplo). VFP automáticamente compara oldval() y curval() para cada campo. Si detecta alguna diferencia, nosotros tenemos un conflicto..
  2. El usuario ha entrado un duplicado del valor una clave primaria o candidata.
  3. Se violó una regla de un campo o tabla, o un campo que no soporta valores null es null.
  4. Falló un desencadenante (trigger).
  5. Otro usuario ha bloqueado el registro. Esto puede minimizarse evitando el bloqueo manual de registros con rlock() y usando el mismo mecanismo de bloqueo para buffer, para una tabla, en todos los formularios y programas que la acceden.
  6. Otro usuario cambió el estado borrado del registro.
Usted debe decidir qué hacer cuando tableupdate() falla. También, si su aplicación le permite al usuario hacer clic en botones “Siguiente” o “Anterior” mientras está editando un registro y aquellas funciones no usan tableupdate(), usted debe manejar el error que ocurrirá cuando se intenta el guardado automático. En ambos casos, el lugar apropiado para manejar esto en un una rutina que atrape los errores.

El manejo de errores ha sido mejorado en VFP. La vieja forma para atrapar un error (la cual usted aún puede usar en VFP) es usar el comando on error para especificar un procedimiento que se ejecute cuando ocurre un error. Esta rutina de error podría típicamente podría mirar en error() y message() para determinar qué sucedió, y tomar la acción apropiada. VFP ahora provee un mecanismo automático de manejo de errores: el método Error. Si existe un método Error para un objeto o formulario, él se ejecutará automáticamente cuando ocurre un error sin tener que atraparlo manualmente. aerror() es una nueva función que ayuda a entender que está mal. Usted le pasa un nombre de matriz y el crea o actualiza la matriz con los siguientes elementos:
Elemento Tipo Descripción
1 Numeric El número de error (la mismo que error()).
2 Character El mensaje de error (lo mismo que message()).
3 Character El parámetro con error (por ejemplo, un nombre de campo) si el error tiene uno (lo mismo que sys(2018)) o .NULL. si ninguno.
4 Numeric or Character El área de trabajo en la cual ocurrió el error si apropiado, de lo contrario .NULL.
5 Numeric or Character El desencadenante que falló (1 para insert, 2 para update, o 3 para delete) si falla un desencadenante (error 1539), o .NULL. si no.
6 Numeric or Character .NULL. (usado para errores OLE y ODBC).
7 Numeric .NULL. (usado para errores OLE).
Por ejemplo, aerror(laERROR) creará o actualizará una matriz llamada laERROR. Aquí están los errores comunes que pueden ocurrir cuando VFP intenta escribir el buffer a la tabla:
Error # Mensaje de Error Comentario
109 Registro está siendo usado por otro
1539 Falló Desencadenante (Trigger) Verifique elemento 5 para determinar cuál trigger falló
1581 Campo no acepta valores nulos Verifique elemento 3 para determinar cuál campo está involucrado.
1582 Violada regla de validación de Campo Verifique elemento 3 para determinar cuál campo está involucrado.
1583 Violada regla de validación de Registro
1585 Registro fue modificado por otro
1884 Violada unicidad de índice. Verifique elemento 3 para determinar cuál campo está involucrado.
El manejo de estos errores es directo: informe al usuario el problema y envíelo al modo editar para corregir el problema o cancelar. Para el error #1585 (registro fue modificado por otro), hay varias formas en que puede manejar el error:
  1. Usted le informa a alguien que otro ha modificado el registro y entonces cancela su edición usando tablerevert(). Yo sospecho que la mayoría de usuarios no estarían felices con esta estrategia <g>.
  2. Puede forzar la actualización del registro usando tableupdate(.F., .T.). Esto causa que se sobre-escriban los cambios de otros usuarios con las modificaciones del usuario actual. Este usuario podría estar feliz, pero los otros usuarios probablemente no lo estarían.
  3. Puede mostrar en una copia del mismo formulario los cambios que el otro usuario ha hecho al registro (fácil de hacer con la habilidad de VFP para crear múltiples instancias del mismo formulario). El usuario puede decidir entonces si los cambios del otro usuario deberían mantenerse o no, y usted puede usar tableupdate(.F., .T.) para forzar la actualización o tablerevert() para cancel.
  4. Un esquema mas inteligente involucra determinar si nosotros tenemos un conflicto “real” o no. Por “real” yo quiero decir: ambos usuarios cambian el mismo campo o no. Si los campos que ellos actualizan son diferentes, podríamos informar a VFP que sólo actualice el campo que este usuario ha cambiado, manteniendo intactos los cambios del otro usuario. Un ejemplo podría se un sistema de proceso de órdenes. Un usuario puede haber editado la descripción de un producto mientras otro usuario ha entrado una orden para el producto, por esta razón disminuye la cantidad en existencia. Estos cambios no son mutuamente excluyentes —si hacemos la actualización de nuestra tabla menos gruesa (esto es, no actualizamos un registro completo al mismo tiempo, sólo los campos que modificados), podemos satisfacer a ambos usuarios.
Aquí está cómo trabaja la lógica:
  1. Hallar un campo donde oldval() es diferente que curval(), esto significa que el campo fue editado por otro usuario. Si el valor buffered de los campos es el mismo de oldval(), este usuario no puede cambiar el campo, así podemos evitar sobre-escribir su nuevo valor poniendo el valor buffered como curval().

  2. Hallar un campo donde el valor buffered sea diferente a oldval(). Este es un campo que el usuario ha modificado Si oldval() es igual a curval(), el otro usuario no ha cambiado este campo, así podemos sobre-escribirlo con seguridad.

  3. Si hallamos un campo donde el valor buffered es diferente del valor oldval() pero es el mismo de curval(), ambos usuarios hicieron el mismo cambio. Esto puede verse improbable, un ejemplo puede ser cuando alguien envía un aviso de cambio de domicilio a una compañía y de algún modo dos usuarios deciden poner al día el registro al mismo tiempo. Debido a que los cambios son idénticos, nosotros podríamos estar habilitados a sobre-escribir el campo. Sin embargo, en el caso de una cantidad que está siendo actualizada por la misma cantidad (por ejemplo, dos órdenes por la misma cantidad fueron entradas al mismo tiempo), usted no desearía sobre-escribir el campo, y debería considerar esto como un conflicto “real”.
  4. Si tenemos un caso donde el valor buffered de un campo es diferente de ambos oldval() y curval(), además oldval() y curval() no son los mismos (iguales), ambos usuarios han cambiado el mismo campo pero con diferentes valores. En este caso, tenemos un conflicto “real”. Usted tiene que decidir cómo manejar el conflicto.
  5. En el caso de una cantidad de inventario existente o cuenta de balance, una posibilidad es aplicar al valor buffered el mismo cambio que el otro usuario ha hecho. Por ejemplo, si oldval() es 10 y curval() es 20, el otro usuario ha incrementado la cantidad por 10. Si el valor buffered es 5, este usuario está disminuyendo la cantidad en 5. El nuevo valor buffered sin embargo debería ser value + curval() - oldval(), o 15.
  6. En el caso de campos Fecha, las reglas de negocios y el sentido común podrían ayudar. Por ejemplo, en un programa de citas de pacientes con un campo conteniendo la fecha de la próxima visita de un paciente, la primera de las dos fechas en conflicto es probablemente la correcta para usar, a menos que sea anterior a la fecha actual, en cuyo caso la fecha posterior será la correcta.
  7. Otros tipos de campos, específicamente campos Character y Memo, a menudo no pueden resolverse sin preguntar al usuario que decida si sobre-escribe los cambios del otro usuario o abandona los suyos. Permitiéndole al usuario ver los cambios del otro usuario (como se mencionó antes) puede ayudarlo a tomar la decisión.
Aquí tiene algún código que resuelve este tipo de conflictos (este código asume que nosotros hemos determinado que el problema es un error #1585, el registro ha sido modificado por otro usuario):

* Verifica cada campo para ver cuál tiene un conflicto.
llConflict = .F.
for lnI = 1 to fcount()
   lcField      = field(lnI)
   llOtherUser  = oldval(lcField)   <> curval(lcField)
   llThisUser   = evaluate(lcField) <> oldval(lcField)
   llSameChange = evaluate(lcField) == curval(lcField)
   do case
     * Otro usuario ha editado este campo pero este usuario,
     * no ha grabado los nuevos valores.
     case llOtherUser and not llThisUser
        replace (lcField) with curval(lcField)
     * Otro usuario no ha editado este campo, o ambos han hecho
     * el mismo cambio, así no necesitamos hacer algo.
     case not llOtherUser or llSameChange
         * Uh-oh, ambos usuarios han cambiado este campo, pero a
         * diferentes valores.
     otherwise
         llConflict = .T.
     endcase
next lnI
* Si tenemos un conflicto, manejarlo.
if llConflict
  lnChoice = messagebox('Otro usuario también cambió este ' + ;
             'registro. Desea sobre-escribir sus cambios (Si), ' + ;
             'no sobre-escribir pero ver los cambios (No), o cancelar ' + ;
             'sus cambios (Cancelar)?', 3 + 16, ;
             'Problema Guardando Registro!')
   do case
   * Sobre-escribir los cambios.
      case lnChoice = 6
         = tableupdate(.F., .T.)
      * Ver los cambios: trayendo otra instancia del formulario.
      case lnChoice = 7
         do form MYFORM name oName
      * Cancelar los cambios.
      otherwise
         = tablerevert()
      endcase
    * Sin conflictos, entonces forzar la actualización.
else
    = tableupdate(.F., .T.)
endif llConflict

18 de diciembre de 2017

Buffering de Datos y Multiusuarios (Parte I)

Autor: Doug Hennig
Traducido por: Germán Giraldo G.

Generalidades

En FoxPro 2.x, los desarrolladores editaban los registros usando scatter memvar, editando las variables de memoria, y gather memvar. El propósito de de esta edición indirecta de campos era proteger el registro haciendo buffering. Con Visual FoxPro, el buffering de datos está incluido, así los campos pueden editarse directamente. Esta sesión discutirá cómo trabaja el buffering de datos y explora estrategias para seleccionar cuál mecanismo de buffering usar y cómo manejar conflictos multiusuario.


Introducción

Si usted ha pasado algún tiempo trabajando con VFP, una de las cosas que probablemente ha aprendido es que aunque usted puede continuar haciendo las cosas en la “vieja” forma si usted lo desea, VFP le da una forma mejor para realizar la misma tarea. Cómo editar los registros en un formulario es un perfecto ejemplo de esto.

Aquí está la “vieja” forma que yo usaba para escribir código para editar un registro en una pantalla de entrada de datos:

  1. La pantalla tiene objetos get para variables de memoria con el mismo nombre de los campos de la tabla (por ejemplo, M.CUST_ID y M.NAME).
  2. Cuando el usuario posiciona la tabla en un registro particular (por ejemplo, usando el botón “Siguiente”), usar scatter memvar para transferir el registro a las variables de memoria y show gets para refrescar los valores en la pantalla. El usuario no puede editar las variables (ellas están deshabilitadas o tienen una cláusula when que se evalúa a F.) a causa de que el usuarios actualmente está en el modo “ver”.
  3. Cuando el usuario elige el botón “Editar”, trata de bloquear el registro; muestra un mensaje apropiado si no podemos. Verificar el valor de cada campo contra su variable de memoria —si ellos no coinciden, otro usuario podría haberlo editado y guardado el registro desde que nosotros lo mostramos por primer vez. En ese cado, muestra un mensaje apropiado y usa scatter memvar y show gets de nuevo para que el usuario vea el contenido actual del registro.
  4. Si los campos y las variables de memoria coinciden, se habilitan los objetos get o hace que su cláusula when se evalúe a .T. para que el usuario pueda editar las variables.
  5. Cuando el usuario elige el botón “Guardar”, se hace alguna validación para asegurar que todo se entró de acuerdo a sus reglas, entonces usar gather memvar para actualizar el registro desde las variables de memoria y desbloquea el registro. Deshabilitar los objetos get o hacer que su cláusula when se evalúe a .F. para que el usuario esté de nuevo en el modo “ver”.
Note que en este esquema nosotros no hacemos un read directo contra el registro. En su lugar, nosotros le permitimos al usuario editar variables de memoria y sólo copia aquellas variables de memoria al registro si todo está bien. La razón para usar este método es proteger la tabla; nosotros no permitimos que cualquier datos se almacene a menos que pase todas la reglas. También note que el registro está bloqueado mientras el usuario está editando las variables de memoria. Esto evita que otros usuarios editen el mismo registro al mismo tiempo. Hacerlo, sin embargo, adolece del síndrome de “salí a comer” —si el usuario empieza a editar, entonces se va a comer, el registro permanece bloqueado, no disponible para que otros usuarios lo editen. Desde luego, esta no es la única forma de editar registros. Usted podría hacer el bloqueo justo antes de guardar el registro en lugar de cuando inicia el modo editar. Esto minimiza el tiempo que el registro permanece bloqueado, permitiéndole a otros usuarios accederlo. Esto tiene sus propios inconvenientes, aunque: si el usuario edita las variables y hace clic en “Guardar”, qué sucede si mientras tanto algún otro usuario ha editado el registro? Usted sobre-escribe los cambios? Usted evita que el registro se guarde? Esto es un asunto de diseño que usted debe manejar caso-por-caso. El único propósito de todo este esfuerzo es proteger los datos. Si usted estuviera escribiendo una aplicación que sólo usted usará siempre, usted probablemente lo haría mucho mas simple —sólo un read contra el campo directamente en el registro. Esto hace que la pantalla actúe en forma equivalente a un browse, debido a que todo lo que usted digita se hace directamente en el registro. Sin embargo, debido a que nosotros no podemos confiar en esos usuarios molestos para saber lo que ellos pueden y no pueden entrar, nosotros tenemos que proteger los datos construyendo un "cortafuego" entre el usuario y la tabla. Crear este "cortafuego" en FoxPro 2.x tomaba una cantidad considerable de código. VFP provee un mecanismo “cortafuegos” que nos da lo mejor de ambos mundos: read directo contra un registro mientras que sólo permite que los datos se escriban después de pasar todas las pruebas. Este mecanismo es buffering.

Buffering

Usar variables de memoria para mantener el contenido de un registro puede considerarse como crear un buffer de datos. Los datos se transfieren desde el registro al “buffer” usando scatter memvar y desde el “buffer” al registro con gather memvar. VFP no sólo puede hacer este tipo de buffering de un único registro (llamado buffering de fila o de registro) automáticamente, él también soporta otro tipo de buffering (llamado buffering de tabla) en el cual múltiples registros se accedan a través de un buffer. El buffering de registro se usa normalmente cuando usted desea acceder o actualizar un único registro a la vez. Esto es común en mecanismos de entrada de datos como los descritos antes: el usuario puede mostrar o editar un único registro en el formulario. El buffering de tabla debe elegirse para actualizar varios registros a la vez. Un ejemplo común es un formulario con cabecera-detalle de factura. Usando buffering de tabla para la tabla detalle de factura, usted puede permitirle al usuario editar tantas líneas de detalle como él desee, y entonces guardar o cancelar todos los registros de detalle a la vez. Además de los dos mecanismos de buffering, hay dos mecanismos de bloqueo. La “vieja” forma que describí antes puede considerarse un esquema de bloqueo pesimista —el registro se bloquea tan pronto como el usuario elige “Editar”, y permanece bloqueado hasta que él elige “Guardar”. Esto asegura que nadie mas puede hacer cambios al registro mientras el usuario lo esté haciendo, esta forma puede o no ser una buena cosa, dependiendo de su aplicación. El otro método que yo describí antes es un mecanismo de bloqueo optimista —el registro sólo se bloquea por el breve tiempo que toma escribir el registro, y se desbloquea inmediatamente. Esto maximiza la disponibilidad del registro (esto también se conoce como maximizar la simultaneidad) pero significa que tenemos que manejar conflictos que ocurren si dos usuarios editan el registro al mismo tiempo. Como veremos en un momento, actualmente esto es fácil de hacer en VFP, así el buffering optimista probablemente será el mecanismo elegido para la mayoría de las aplicaciones. Debido a que los registros pueden ser buffered automáticamente, ya no es necesario usar el mecanismo de “buffer manual”. En otras palabras, ahora nosotros podemos hacer read directamente contra los campos en el registro sin preocuparnos por el mantenimiento de variables de memoria para cada uno. Para guardar los cambios, simplemente le informamos a VFP que escriba el buffer en la tabla, y para cancelar los cambios, le informamos que no lo haga. En un momento veremos cómo hacerlo. VFP implementa el buffering creando un “cursor” cuando se abre una tabla. El cursor se usa para definir propiedades para la tabla. En el caso de tablas locales, la única propiedad para el cursor es cómo se realiza el buffering; para vistas y tablas remotas hay propiedades adicionales que van mas allá del alcance de esta sesión. Estas propiedades se establecen usando la función cursorsetprop() y se examinan con cursorgetprop(). Veremos resumidamente el uso de estas funciones. El buffering de tabla tiene una implementación interesante respecto a agregar registros: cómo se agregan los registros al buffer, se les asigna un número de registro negativo. recno() devuelve -1 para el primer registro agregado, -2 para el segundo, y así sucesivamente. Puede usar go con un número negativo para posicionar el buffer en el registro agregado apropiado. Esto tiene una implicación en las rutinas que manejan números de registro —en lugar de probar para between(lnRecno, 1, reccount()) para asegurar que lnRecno es un número de registro válido, usted ahora probará para between(lnRecno, 1, reccount()) or lnRecno < 0.
Usar Buffering
Buffering está desactivado por definición, así VFP actúa como FoxPro 2.x en términos de cómo se escriben las actualizaciones en una tabla. Para usar buffering, usted debe activarlo específicamente. Buffering está disponible para ambos: tablas libres y aquellas unidas a una base de datos. Buffering requiere que usted establezca set multilocks on debido a que por definición también está desactivado; usted obtendrá un mensaje de error si olvida hacer esto. Puede poner multilocks = on en su CONFIG.FPW o usar la función Options en el menú Herramientas para guardar esta configuración por definición. Buffering se controla usando cursorsetprop('Buffering', <n>, <Alias>). No tiene que especificar <Alias> si está configurando buffering para la tabla actual, <n> es uno de los siguiente valores dependiendo del método de buffering y bloqueo que desee usar:


Buffering/Método de Bloqueo <n>
sin buffering 1
registro, pesimista 2
registro, optimista 3
tabla, pesimista 4
tabla, optimista 5
Por ejemplo, para habilitar buffering de registro optimista, use cursorsetprop('Buffering', 3). Para determinar qué buffering está en uso en la tabla actual, use cursorgetprop('Buffering'). Para habilitar buffering en un formulario, podría especificar cursorsetprop() para cada tabla en el método Load del formulario, pero la estrategia preferida es establecer la propiedad BufferMode del formulario como optimista o pesimista (el predefinido es “ninguno”). El formulario entonces automáticamente usa buffering de tabla para todas las tablas ligadas a grids y buffering de registro para todas las otras tablas. Si usa un DataEnvironment para el formulario, puede sobre-escribir el BufferMode del formulario para una tabla particular estableciendo su BufferModeOverride como desee. Mientras un usuario esté cambiando los datos de un registro buffered (esté en medio de la edición de un registro), usted tiene acceso no sólo al valor que él ha entrado en cada campo, si no también al valor anterior de cada campo y su valor actual (el valor actualmente en disco). Dos nuevas funciones, oldval() y curval(), fueron agregadas para este propósito. Aquí está cómo se obtienen los valores apropiados:
Para obtener: Use:
el valor que el usuario ha entrado (el valor en el buffer) <fieldname> or <alias.fieldname>
el valor antes que el usuario cambiara algo oldval('<fieldname>')
el valor actual en el registro curval('<fieldname>')
curval() y oldval() sólo pueden usarse con buffering optimista. Usted puede extrañarse que el valor devuelto por curval() podría diferir del devuelto por oldval(). Esto obviamente no ocurriría si sólo un usuario está ejecutando la aplicación. Sin embargo, en una red y con bloqueo optimista, es posible que después de que el usuario ha iniciado la edición de un registro, otro usuario ha editado el mismo registro y ha guardado los cambios. Aquí hay un ejemplo: Bob trae el registro #2 de CONTACTS.DBF y hace clic en el botón “Editar”:
Campo Valor Oldval() curval()
LAST_NAME Jones Jones

Jones

FIRST_NAME Bill Bill Bill


Bob cambia el primer nombre a Sam pero todavía no ha guardado el registro:

Campo Valor Oldval() curval()

LAST_NAME

Jones Jones Jones
FIRST_NAME Sam Bill Bill<

Mary trae el registro #2 de CONTACTS.DBF, hace clic en el botón “Editar”, cambia el primer nombre a Eric, y guarda. En la máquina de Bill:

Field

Value

Oldval()

curval()

LAST_NAME

Jones

Jones

Jones

FIRST_NAME

Sam

Bill

Eric

Observe que FIRST_NAME, oldval('FIRST_NAME'), y curval('FIRST_NAME') devuelven valores diferentes. Teniendo acceso al valor original, el valor buffered, y el valor actual para cada campo en el registro, usted puede:
  1. determinar cuáles campos cambió el usuario comparando el valor buffered con el valor original; y
  2. detectar si otros usuarios en la red han hecho cambios al mismo registro después de iniciar la edición, comparando el valor original con el valor actual.
Si no le interesan los valores anterior y actual y sólo desea detectar si un campo fue editado por el usuario, use getfldstate(). Esta nueva función devuelve un valor numérico indicando si algo del registro actual ha cambiado. getfldstate() se llama en la siguiente forma: getfldstate(<FieldName> | <FieldNumber> [, <Alias> | <WorkArea>]) y devuelve uno de los siguientes valores:
Valor

Descripción

1 Sin cambios
2 El campo fue editado el estado de borrado del registro ha cambiado
3 Se agregó un registro pero el campo no fue editado y el estado de borrado no ha cambiado.
4 Se agregó un registro y el campo fue editado o el estado de borrado del registro ha cambiado.
Cambiar el estado de borrado significa ambos: borrar o recuperar (delete/recall) el registro. Note que borrar y recuperar inmediatamente el registro resultará en un valor de 2 o 4 aunque no haya un efecto de red para el registro. Si no especifica un alias o área de trabajo, getfldstate() opera sobre la tabla actual. Especifique 0 para <FieldNumber> para devolver el estado de agregado o borrado del registro actual. Si especifica -1 para <FieldNumber>, la función devolverá una cadena con el primer dígito representando el estado de la tabla y un dígito para el estado de cada campo. En el ejemplo mencionado antes, donde Bill edita el segundo campo, getfldstate(-1) podría devolver “112”. El primer dígito indica que el registro no fue agregado o borrado, el segundo que el primer campo no ha cambiado, y el tercero que el segundo campo ha cambiado.

Escribir un Registro Buffered

Continuando con el ejemplo anterior, ahora Bill hace clic en el botón “Guardar”. Cómo informamos a VFP que escriba el buffer al registro? Con buffering de registro, la tabla se actualiza cuando usted mueve el puntero del registro o usa la nueva función tableupdate(). Con buffering de tabla, moviendo el puntero del registro no actualiza la tabla (debido a que todo el punto del buffering de tabla es que varios registros son buffered al mismo tiempo), así la forma usual es llamar la función tableupdate(). Es mejor usar tableupdate() aún para buffering de registro debido a que usted tiene mas control sobre lo que sucede. tableupdate() devuelve .T. si el buffer se escribió con éxito al registro. Si el buffer de registro no ha cambiado (el usuario no ha editado algún campo, agregado un registro, o cambiado el estado de borrado para el registro), tableupdate() devuelve .T. pero no hace nada. tableupdate() puede recibir unos pocos parámetros opcionales: tableupdate(<AllRows>, <Forced>, <Alias> | <Workarea>) El primer parámetro indica qué registros actualizar: .F. informa que sólo se actualice el registro actual, mientras que .T. significa actualizar todos los registros (sólo tiene efecto si se usa buffering de tabla). Si el segundo parámetro es .T., cualquier cambio de otro usuario se sobre-escribirá por los cambios del usuario actual. A menos que se especifique el tercer parámetro, tableupdate() actualizará la tabla actual. Cómo cancelar los cambios que ha hecho el usuario? Con la estrategia de variables de memoria, usted sólo usa scatter memvar de nuevo para restaurar las variables de memoria a los valores almacenados en disco. Con buffering, use la función tablerevert() para realizar lo mismo para el buffer.

14 de diciembre de 2017

Extraer el oro del XSource - Parte 3

Artículo original: Mining for Gold in XSource - Part 3
http://msdn.microsoft.com/library/en-us/dnfoxtk04/html/ft04l6.asp
Autor: Doug Hennig
Traducido por: Ana María Bisbé York


En las dos primeras partes de esta serie, Doug Hennig examinó algunos componentes interesantes de XSource, el código fuente de la mayoría de herramientas "Xbase" que vienen con VFP.  Este último artículo examina dos componentes: una clase como región con desplazamiento y un framework para la creación de generadores propios.

Como he descrito en los dos artículos anteriores, el código fuente para casi todas las herramientas XBase viene con VFP en un archivo .ZIP: XSource.ZIP en la carpeta Tools\XSource debajo de la carpeta raíz de VFP. Al descomprimir este archivo obtenemos como resultado una carpeta llamada VFPSource que queda debajo de Tools\XSource que contiene el código fuente para las herramientas XBase.

En la Parte 1, he comentado sobre algunas de las imágenes que se incluyen en el XSource y componentes que puede utilizar para guardar la configuración de los recursos de VFP y para crear menús contextuales orientados a objetos. La Parte 2 describió cómo mostrar archivos de videos en formularios VFP, crear diálogos de progreso y utilizar un control que imite la popular barra del control Outlook.

En este artículo, el último de la serie, voy a mostrar cómo crear una región desplazable en un formulario VFP y examinaré un framework para la creación de generadores propios.

Crear una región con desplazamiento (scrolling)

Puede ser que haya necesitado mostrar controles en una región desplazable. Por ejemplo, suponga que permite que sus usuarios añadan sus propios campos a la aplicación. Esta es una gran cualidad debido a que permite a sus usuarios personalizar la aplicación acorde a sus necesidades. El problema es entonces, cómo mostrar los campos personalizables. Si hay pocos campos, no es problema, porque van a caber en un formulario. Si tiene muchos campos, podría utilizar un formulario con desplazamiento. Sin embargo, ¿qué pasaría si necesita mostrar estos campos en un formulario donde además hay controles no desplazables? En ese caso, necesita una región con desplazamiento con los campos a personalizar dentro de ella.

La herramienta Lista de tareas es una aplicación relativamente poco utilizada que salió con VFP desde VFP 7.0. Permite agregar campos de usuario y mostrarlos en una región con desplazamiento en la página Fields (Campos) del diálogo Task Properties (Propiedades de tareas) (vea Figura 1)

Figura 1

La región con desplazamiento es una instancia de la clase cntScrollRegion en TaskListUI.VCX en la carpeta TaskList XSource. La clase cntScrollRegion consiste en un contenedor (donde irán los controles) y un control Microsoft Flat ScrollBar (necesita distribuir MSCOMCT2.OCX con su aplicación para poder brindar este control activex). Cada control añadido al contenedor (por ejemplo, las etiquetas y cuadros de texto en la Figura 1) es una instancia de una clase desplazable (como edtScrolling), también desde TaskListUI.VCX. Se requieren clases especiales en lugar de clases bases de tal forma que cuando el usuario se mueva de un control al siguiente, la región con desplazamiento se desplace automáticamente si es necesario.

Veamos cómo utilizar estas clases en nuestras propias aplicaciones. Decidí crear campos propios a la tabla Customer en la base de datos de ejemplo Northwind. En lugar de agregar campos a la tabla directamente, lo que causa problemas cuando quiero actualizar la estructura de la tabla cuando instalo una actualización de mi aplicación, he creado una tabla adicional, CustomerFields, que tiene el campo CustomerID que se corresponde con el valor CustomerID de un registros en Customers, además los campos que desee mi usuario agregar. Otra tabla, CustFldDef, define los campos que el usuario va agregando (presumiblemente, yo proporciono un diálogo en el que el usuario puede definir los campos y actualizar su contenido de CustFldDef si fuera necesario). Esta tabla tiene los campos FIELDNAME, FIELDTYPE, FIELDLEN, FIELDCAPTION, ORDER y PICTURE, la que guarda la información con la definición de cada campo de usuario.

SFCustomFieldsForm en SFXSource.VCX, es una clase base basada en formulario con un cntScrollRegion dentro y los botones OK y Cancel.( El botón OK se llama cmdApply, porque algunos métodos en la clase Task List lo requieren). El método Init abre la tabla de campos de usuario (para hacerlo más genérico, el nombre de la tabla, el orden del índice a utilizar, y el nombre del campo clave están contenidos en las propiedades  cCustomFilesTable, cCustomFilesOrder y cCustomFilesKey en lugar de estar escrito en el código) y llama al método SetupCustomFields para configurar la región de desplazamiento.

He aquí el código para SetupCustomFields

local lnSelect, ;
  lcAlias, ;
  lnTop, ;
  lcField, ;
  lcType, ;
  lnLen, ;
  lcCaption, ;
  lcPicture, ;
  lcClass, ;
  lcTemplate, ;
  lcLabel, ;
  loLabel, ;
  loField
with This.cntScrollRegion
  * El control de desplazamiento TaskList requiere que el formulario
  * tenga un miembro Task cuyas propiedades se correspondan 
  * con los nombres de archivos. Entonces, lo creamos
  This.AddProperty('Task', createobject('Empty'))
  * Abrir la tabla que contiene información acerca de 
  * los campos de usuario y recorrer cada registro.
  lnSelect = select()
  select 0
  use (This.cMetaDataTable) again shared order ORDER
  lcAlias = alias()
  lnTop = 10
  scan
    * Tomar información sobre cada campo.
    lcField = trim(FIELDNAME)
    lcType = FIELDTYPE
    lnLen = FIELDLEN
    lcCaption = trim(CAPTION)
    lcPicture = trim(PICTURE)
    lcTemplate = ''
    * Determinar qué clase utilizar para la entrada de datos
    * basado en el tipo de datos. Para los campos Date y DateTime, 
    * puede utilizar las clases oleDateScrolling y oleTimeScrolling, 
    * excepto que ellos no pueden controlar las fechas en blanco.
    do case
      case lcType = 'L'
        lcClass = 'chkScrolling'
      case lcType = 'M'
        lcClass = 'edtScrolling'
      case lcType $ 'NFIBY'
        lcClass = 'txtScrolling'
        lcTemplate = lcPicture
      otherwise
        lcClass = 'txtScrolling'
        lcTemplate = replicate('N', lnLen)
    endcase
    *Si no estamos utilizando una casilla de verificación (checkbox), crear una etiqueta.
    if lcClass <> 'chkScrolling'
      lcLabel = 'lbl' + lcField
      .cntPane.NewObject(lcLabel, 'labScrolling', ;
        home() + 'Tools\XSource\VFPSource\TaskList\' +;
        'TaskListUI.vcx')
      loLabel = evaluate('.cntPane.' + lcLabel)
      with loLabel
        .Top = lnTop
        .Left = 10
        .Caption = lcCaption
        .FontName = This.FontName
        .FontSize = This.FontSize
        .Visible = .T.
        lnTop = lnTop + .Height + 4
      endwith
    endif lcClass <> 'chkScrolling'
    * Crea un control para utilizar entrada de datos para este campo 
    cntPane.NewObject(lcField, lcClass, ;
      home() + 'Tools\XSource\VFPSource\TaskList\' + ;
      'TaskListUI.vcx')
    loField = evaluate('.cntPane.' + lcField)
    with loField
      if lcClass = 'chkScrolling'
        .Caption = lcCaption
      endif lcClass = 'chkScrolling'
      .Top = lnTop
      .Left = 10
      .Visible = .T.
      lnTop = lnTop + .Height + 10
      .ControlSource = This.cCustomFieldsAlias + '.' +;
        lcField
      if inlist(lcClass, 'oleDateScrolling', ;
        'oleTimeScrolling')
        .Font.Name = This.FontName
        .Font.Size = This.FontSize
      else
        .FontName = This.FontName
        .FontSize = This.FontSize
      endif inlist(lcClass ...
      if not empty(lcTemplate)
        .Width = txtwidth(lcTemplate, This.FontName, ;
          This.FontSize) * ;
          fontmetric(6, This.FontName, This.FontSize) + 12
      endif not empty(lcTemplate)
      if lcClass = 'txtScrolling'
        .InputMask = lcPicture
      endif lcClass = 'txtScrolling'
    endwith
    * Agrega una propiedad enlazando el nombre del campo al miembro Task
    addproperty(This.Task, lcField)
  endscan
  * Refresca la región de desplazamiento.
  .cntPane.Height = lnTop
  .SetViewPort()
  * Restablecemos.
  use
  select (lnSelect)
endwith

Comienza agregando una propiedad nueva al formulario llamada Task y colocar en ella un objeto Empty. Las clases Task List requieren que esta propiedad del formulario contenga un objeto cuyas propiedades se correspondan con el nombre en los campos de usuario (esto se utiliza para el enlace con los datos en lugar de los campos de la tabla; pero en realidad nosotros vamos a enlazar directamente a los campos,) así que luego agregaremos las propiedades al objeto Empty para corresponder con sus nombres.

El código abre la tabla que contiene el metadato para los campos de usuario, el nombre de los cuales se guarda en la tabla cMetaData en lugar de ser escrito en el código determina que clase *Scrolling utilizar como control para cada campo basado en su tipo de datos, y si el control no es una casilla de verificación (checkbox), añade una etiqueta a la región de desplazamiento (utilizando la clase lblScrolling). Luego, agrega el control apropiado para el campo y establece varias propiedades incluyendo ControlSource, el que enlaza el control al campo, en la tabla de campos de usuario.

Una propiedad con el mismo nombre del campo, se agregó al objeto Empty contenido en la propiedad Task, como se ha dicho antes. Finalmente, después de que todos los controles fueron agregados, el código establece la altura de la región de desplazamiento, según sea necesario (posiblemente más alta que el formulario si hay muchos controles) y llama al método SetPreviewPort del objeto cntScrollRegion, el que ajusta el tamaño, posición y otros atributos de la barra de desplazamiento, según sea necesario.

Hay un error (bug) en el método SetViewPort de cntScrollRegion, que impide que la barra de desplazamiento aparezca adecuadamente. Comente la siguiente línea de código:

This.oleScrollBar.LargeChange = This.Height - 20

TestScrolling.SCX es un formulario basado en SFCustomFieldsForm, su cCustomFilesTable, cCustomFilesOrder, cCustomFilesKey y cMetaDataTable son establecidos con sus valores apropiados para utilizar las tablas CustomerFileds y CustFldDef. Puede incluso ejecutar este formulario y pasarle un valor CustomrID desde la tabla Customers (por ejemplo "ALFKI") o ejecutar el formulario TestToolBox que hemos explicado en la Parte 2 de esta serie, seleccione el módulo Customers, seleccione un usuario de la lista, y haga clic sobre el botón Edit Custom Fields. La figura 2 muestra cómo el formulario TestScrolling aparece cuando es parcialmente desplazado hacia abajo.

Figura 2

Crear sus propios generadores

La tecnología de generadores existe en VFP desde su liberación. Sin embargo, los generadores son subutilizados, parcialmente debido a que mucha gente piensa que son difíciles de crear. Mientras esto no es realmente cierto, existen muchas cosas que controlar al crear un generador, tales como obtener una referencia a un objeto que será mantenido por el generador, controlar los aspectos de su modalidad (si el generador es no modal, se debe cerrar cuando se cierre el formulario o la clase), y así sucesivamente. Afortunadamente, XSource, una vez más acude a nuestro rescate.

XSource viene con un framework completo para la creación de generadores. No está en su propio directorio; pero en su lugar, en parte de los generadores CursorAdapter y DataEnvironment (en la carpeta DEBuilder hija de la carpeta XSource Wizards). Este framework facilita la creación de generadores propios, ya que controla los siguientes aspectos:

  • Puede ser llamado desde el Builder.APP (incluso como un generador registrado en la tabla Builder en la carpeta Wizards del directorio raíz de VFP, o especificado en la propiedad Builder para un objeto) o como un formulario standalone.
  • Admite diálogos modales y no modales.
  • Controla todos los aspectos internos, tales como mantenimiento de las referencias de objetos a ser mantenidos.
  • Se auto-refresca al activar, entonces al establecerlo en la ventana Propiedades, hace los cambios, y reactiva los cambios al generador y actualiza el generador automáticamente.
  • Se cierra automáticamente si se cierra la clase o el formulario.
  • Controla los cambios revertidos al hacer clic en el botón Cancelar (Cancel).

No voy a detallar aquí cómo trabaja el generador o cómo es su arquitectura; pero lo interesante acerca del generador es que no haya que entender esos aspectos para crear un generador útil. Los únicos detalles que se necesitan conocer son los siguientes:

  • Para crear un generador, subclasear la clase BuilderForm en BuilderControls.VCX. Los controles que utiliza en este formulario se pueden instanciar de otras clases en BuilderControls.VCX ( tales como BuilderLabel o BuilderTextbox); pero ellos no tienen que estar, usted utiliza los controles si desea.
  • La manera más sencilla de especificar qué generador es usado por una clase en particular agregue una propiedad de usuario Builder de la clase y llenar con el nombre de la clase a utilizar como el generador y la biblioteca de clases contenido en el, utilizando el formato: Library, Class.

Vamos a echar un vistazo a un ejemplo, donde el Generador va a ser muy útil. SSFile es una clase en SFCCtrls.VCX que proporciona un control donde un usuario puede escribir un fichero o hacer clic sobre un botón para que se muestre un cuadro de diálogo desde el cual el o ella pueda seleccionar el fichero. Esta clase consiste en una etiqueta, que ofrece un texto para el control, un cuadro de texto, que contiene el nombre del archivo y una instancia del objeto SFGetFile, una clase en SFButton.VCX que muestra un cuadro de diálogo Abrir fichero.

Para utilizar SFFile, simplemente arrástrela a un formulario y establezca los valores para sus propiedades, así cómo para cExtensions (una lista de extensiones permitidas en el cuadro de diálogo), cCaption (Encabezado para el cuadro de diálogo Abrir) y cControlSource (algo que enlaza el control con el generador si se desea). Sin embargo, hay un par de cosas molestas:

  • Para fijar la propiedad Caption de la etiqueta, hay que hacer Clic derecho sobre el objeto SFFile, seleccionar Modificar y luego hacer Clic sobre la etiqueta, ir a la ventana Propiedades, seleccionar la propiedad Caption y escribir el nuevo texto. Por supuesto, tendrá que mover el cuadro de texto y los botones SFGetFile hasta tanto sea necesario para que quepa el texto de la etiqueta y es posible que necesite redimensionar el objeto SFFile si resulta muy pequeño.
  • Si desea alargar o acortar el objeto SFFile, lo primero que hace falta es redimensionarlo, luego colocarse dentro de el y redimensionar el cuadro de texto y mover el botón SFGetFile a la posición adecuada.

Nada de eso es un trabajo agotador; pero puede llevar un tiempo hacerlo. Como todas aquellas pequeñas tareas que toman "pequeños momentos" en su labor diaria. Cualquier cosa que podamos hacer por incrementar nuestra productividad, especialmente para tareas de este tipo, es bienvenida.

He aquí algunos pasos para crear un generador para SFFIle:

1. Como esta clase tiene una propiedad de usuario Builder, no necesita agregar una. Sin embargo, llénela con el nombre de la clase a utilizar como generador y la librería de clases que la contiene, utilizando el formato Library,Class.

2. Cree una subclase de BuilderForm utilizando los nombres de la clase y la librería de clases que ha especificado en SSFleBuilde. Establezca Caption igual aSSFFile Builder.

3. Agregue un objeto BuilderLabel y establezca su Caption igual a \<Label caption.

4. Agregue un objeto BuilderTextbox  al lado de la etiqueta, haga su ControlSource para Thisform.oSource.lblFile.Caption y coloque el siguiente código en su método Valid.(Thisform.oSource es una referencia a un objeto que será mantenido por el generador, de tal forma que puede acceder a cualquier propiedad o miembro del objeto a través de esa referencia.) Este código automáticamente ajusta las propiedades Left y Width del cuadro de texto en el fichero SSFFile como sea necesario para que quepa el texto de la etiqueta:

with Thisform.oSource
  .txtFile.Left = .lblFile.Width + 5
  .txtFile.Width = .cmdGetFile.Left - .txtFile.Left
endwith

5. Agregue otra BuildLabel y establezca su Caption igual a \<Width.

6. Agregar un objeto BuildSpinner además de una nueva etiqueta, haga su ControlSource igual a Thisform.oSource.Width, y coloque el siguiente código en su método Valid. Este código ajusta la posición del botón SFGetFile y el ancho del cuadro de texto en SFFile para que quepa en el nuevo ancho del control.

with Thisform.oSource
  .cmdGetFile.Left = .Width - .cmdGetFile.Width
  .txtFile.Width = .cmdGetFile.Left - .txtFile.Left
endwith

7. Guardar y cerrar la clase builder (generador).

Para probar este generador nuevo, arrastre el objeto SFFile a un formulario, haga clic derecho y seleccione Builder. Cambie el texto de la etiqueta y la propiedad Width y vea que el objeto SSFile se ajusta adecuadamente. La figura 3 muestra cómo se ve este generador en acción.

Figura 3

Vea que el framework del generador ha cambiado algo con la liberación de VFP 8.0, así que con la versión actualizada (la que viene con VFP 9.0) se incluye en el archivo Dowload que acompaña este escrito.

Conclusiones

Es interesante que muchas de las herramientas que vienen con VFP fueron escritas en VFP y lo mejor es que tenemos el código fuente de ellas. Esto permite cambiar estas herramientas para lograr nuestras necesidades o incluso reutilizar algo del código de estas herramientas en sus propias aplicaciones. En esta serie de tres partes, he detallado algunos de los componentes de XSource que creo son útiles; pero lo invito a que bucee por si mismo en los directorios XSource para que vea la cantidad de técnicas útiles que puede encontrar.

Descarga

Download 412HENNIG.ZIP