21 de diciembre de 2015

Google Calendar API y oAuth 2.0 con Visual FoxPro 9

Aquí les dejo mi regalo de Navidad para todos los colegas de la Comunidad de Visual FoxPro en Español. Felices Fiestas y un excelente Año 2016 para todos!!!

Google Calendar API y oAuth 2.0 con Visual FoxPro 9

José Enrique Llopis
Alicante - España
Kansas City - Missouri - USA
jellopis@rocketmail.com
www.multilinkcrm.com
Mi perfil online: https://es.linkedin.com/in/pepellopis

A - Configurar Google Authentication OAuth 2.0

Ejecute la Google Developers Console https://console.developers.google.com y haga clic en Create a new Project:

Complete los datos del proyecto

Debe activar las API’s que usará en este proyecto, para hacer esto haga clic en el botón Enable and Manage APIs.

Seleccione Google Calendar API

Por supuesto esta técnica le abre la puerta para usar todas las restantes API de Google, no solo Calendar

Haga clic en el botón Enable API, como se muestra en la imagen

¡¡¡MUY IMPORTANTE!!! Debe crear las credenciales oAuth 2.0

Haga clic en Credentials y seleccione oAuth client ID

Ahora, usted podrá configurar la pantalla que Google mostrará al usuario para autorizar la aplicación, para hacer esto haga clic en Configure Consent Screen.

Complete los datos de la pantalla “Consent” y haga clic en Save

La siguiente cosa que debe hacer es seleccionar el tipo de aplicación, en este caso debe elegir la opción OTHER y hacer clic en Create.

Ahora tendrá las credenciales que le permitirán usar la aplicación con Google Calendar

¡¡¡GUARDE ESTOS DATOS!!!

Estos datos los puede ver posteriormente desde la Google Developers Console

B - Configurar la aplicación de ejemplo

Edite el fichero include: ./Include/xGCDefs.h

Debe cambiar donde pone YourClientID y YourClientSecret por las credenciales personalizadas que ha obtenido en el paso anterior

#define GC_CLIENT_ID YourClientID
#define GC_CLIENT_SECRET YourClientSecret

Cambie el texto YourClientID por el dato Client_ID

Cambie el texto YourClientSecret por el dato ClientSecret

Guarde el incluye y ahora podrá ejecutar la aplicación de ejemplo

Haga clic en CONNECT y ESPERE!!!!

Ahora haga clic en el botón Permitir

En este momento usted podrá seleccionar el calendario correcto, por defecto corresponde con la dirección de GMAIL, aunque usted ha podido crear otros adicionales.

Por ultimo podrá ver los datos de los eventos de Calendar en un cursor local de Fox:

NOTA:

No puede duplicar una referencia de evento, incluso si lo borra del calendario

NOTA:

En mis aplicaciones yo uso Web Connection http://www.west-wind.com para hacer las llamadas a Internet, como no es un producto gratuito las he sustituido en este ejemplo por Microsoft.XMLHTTP

C - Descarga del proyecto

La descarga del proyecto completo y la documentación está disponible en el siguiente enlace: GCalconnect.rar (832 KB)

LEGAL DISCLAIMER

THIS SAMPLE CODE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) SUSTAINED BY YOU OR A THIRD PARTY, HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ARISING IN ANY WAY OUT OF THE USE OF THIS SAMPLE CODE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Jose Enrique Llopis
jellopis@rocketmail.com
www.multilinkcrm.com

20 de diciembre de 2015

Imprimir imágenes individuales

Artículo original: PRINT INDIVIDUAL IMAGES
https://vfpimaging.blogspot.com/2006/05/print-individual-images.html
Autor: Cesar Ch.
Traducido por: Ana María Bisbé York


He visto en la Web algunos ejemplos que muestran cómo imprimir imágenes individuales directamente a la impresora. No se por qué; pero en algunas situaciones los ejemplos no funcionan. Posiblemente o probablemente sea ¿un bug del desarrollador? :(

Una de las formas más sencillas y seguras para hacer esta tarea es utilizar el Diseñador de informes nativo, y dejar que VFP se encargue de todo el proceso de impresión.

Debajo hay un ejemplo que recibe un archivo de imagen como un parámetro, crea el informe al vuelo, y agrega un objeto imagen OLE que va a imprimir la imagen seleccionada.

Este ejemplo se basa totalmente en MSKB 895602 "How to print pictures and how to display pictures that are stored in a Blob field in Visual FoxPro 9.0". El código adaptado debe trabajar en cualquier versión de VFP. Gracias a Trevor Hancock y MSDN. El código es fácil de entender. Puede cambiar fácilmente la posición de la imagen cambiando los valores HPOS y VPOS. Si quiere un ejemplo y necesita más información, vea en la Ayuda de VFP el tópico "Understanding and Extending Report Structure"

Guarde el código como PRINTIMAGE.PRG

Para imprimir una imagen, sólo hay que llamar PRINTIMAGE(GETPICT())

LPARAMETERS tcImage
*tcImage = GETPICT()
*--------------------------------------------------------
* Código VFP que muestra cómo imprimir archivos de imagen.
* Código adaptado del artículo 895602 de Microsoft Knowledge Base
* http://support.microsoft.com/kb/895602
*
* La mayor parte de este código y los comentarios son de
* Trevor Hancock, de MS
*--------------------------------------------------------
LOCAL lnArea
lnArea = SELECT()
CREATE CURSOR ReportTemp (ImageFile c(150))
INSERT INTO ReportTemp VALUES (tcImage)
*-- Llama a una función que crea un informe por programación.
*-- Se incluye aquí sólo para garantizar que este ejemplo se puede
*-- ejecutar tal cual, sin pedir al desarrollador que cree un
*-- informe manualmente.
MakeReport()
*-- Asegura que el cursor esté seleccionado,
*-- y luego ejecuta la Presentación preliminar del informe
*-- utilizando para ello, una instancia de nuestro Report Listener.
SELECT ReportTemp
REPORT FORM ___ImageReport PREVIEW
DELETE FILE "___ImageReport.fr*"
SELECT (lnArea)
RETURN
*--------------------------------
*-- Esta función crea un informe por programación
*-- con un control OLE Dependiente y otros archivos. Se incluye solamente
*-- con el propósito de demostrar su funcionamiento para que este
*-- código pueda ser ejecutado tal cual.
*-- Normalmente, usted creará su propio informe manualmente utilizando
*-- el diseñador de informes.
FUNCTION MakeReport
  CREATE REPORT ___ImageReport FROM ReportTemp
  *-- Abre el archivo de informe (FRX) como una tabla.
  USE ___ImageReport.FRX IN 0 ALIAS TheReport EXCLUSIVE
  SELECT TheReport
  *-- Elimina del FRX las etiquetas y campos auto generados
  DELETE FROM TheReport WHERE ObjType = 5 AND ObjCode = 0 && Elimina las etiquetas
  DELETE FROM TheReport WHERE ObjType = 8 AND ObjCode = 0 && Elimina los campos
  *-- Agrega un control Picture/OLE Dependiente al informe añadiendo un registro
  *-- con los valores apropiados. Es más fácil de ver qué valores se
  *-- corresponden con los campos
  *-- GATHER NAME para añadir el registro (al comparar con un comando
  *-- SQL-INSERT) utilizando un objeto basado en la clase EMPTY y más tarde el comando
  LOCAL loNewRecObj AS EMPTY
  loNewRecObj = NEWOBJECT( 'EMPTY' )
  ADDPROPERTY( loNewRecObj, 'PLATFORM', 'WINDOWS' )
  ADDPROPERTY( loNewRecObj, 'Uniqueid', SYS(2015) )
  ADDPROPERTY( loNewRecObj, 'ObjType', 17 ) && "Control Picture/OLE Dependiente "
    ADDPROPERTY( loNewRecObj, 'NAME', 'ReportTemp.ImageFile' ) && Referencia de objeto al objeto IMAGE.
  ADDPROPERTY( loNewRecObj, 'Hpos', 100)
  ADDPROPERTY( loNewRecObj, 'Vpos', 600)
  ADDPROPERTY( loNewRecObj, 'HEIGHT', 100000)
  ADDPROPERTY( loNewRecObj, 'WIDTH', 100000)
  ADDPROPERTY( loNewRecObj, 'DOUBLE', .T. ) && La imagen se centra en el control "Picture/OLE Dependiente"
    ADDPROPERTY( loNewRecObj, 'Supalways', .T. )
  *-- Para el control Picture/OLE Dependiente, el contenido del campo OFFSET especifica si
  *-- Nombre de archivo (0), Nombre de campo General (1), o Expresión (2) es la fuente.
  ADDPROPERTY( loNewRecObj, 'Offset', 2 )
  *-- Añade el registro del control Picture/OLE Dependiente al informe.
  APPEND BLANK IN TheReport
  GATHER NAME loNewRecObj MEMO
  *-- Realiza la limpieza y cierra la tabla del informe.
  PACK MEMO
  USE IN SELECT( 'TheReport' )
ENDFUNC

17 de diciembre de 2015

Controlar dinámicamente los datos de un Grid

Artículo original: Controlling grid data dynamically
http://www.ml-consult.co.uk/foxst-20.htm
Autor: Mike Lewis
Traducido por: Ana María Bisbé York


¿Cómo le puede dar a sus usuarios mayor control sobre los contenidos de un Grid en Visual FoxPro?

Supongamos que desea crear un formulario como el que se muestra en la figura 1. Como ve, utiliza un Grid para mostrar los datos de una tabla Productos. Los usuarios pueden controlar el contenido del Grid de las siguientes formas:

  • Pueden limitar los registros a mostrar en el Grid según la categoría seleccionada.
  • Pueden escoger cuál de los dos campos  - el nombre en Inglés o el nombre original - es el que va a aparecer en la columna Description.
  • Pueden estipular el orden para el Grid.

Figura1: Tres formas para que el usuario controle el Grid

Luego de hacer estas selecciones, el usuario presiona el botón Refresh. Los datos del Grid cambian para refrescar según la selección del usuario.

Está claro, ¿verdad? Entonces, ¿cómo podemos crear este formulario?

Primeras ideas

La primera idea pudiera ser acceder al dato por medio de una vista local. Esto suena razonable, ya que usted puede modificar el contenido de una vista parametrizada. Se puede hacer en la cláusula WHERE de una vista, de esta forma:

WHERE Products.Category = ?lcCat

Aquí, lcCat es una variable que guarda la categoría escogida por el usuario. Si especifica entonces la vista como RecordSource del Grid, el dato se filtrará por la categoría requerida tantas veces como se invoque la vista. La llamada a la función REQUERY() va en el evento Click del botón Refresh del formulario.

Hasta aquí todo bien, en lo relativo a los filtros. Pero no es posible parametrizar los campos a mostrar en las columnas dadas ni el orden de la vista. Es posible recrear toda la vista programáticamente cada vez que el usuario presione el botón Refresh; pero esto no es una solución particularmente elegante. ¿Existe alguna forma más sencilla?

Intentar SQL SELECT

Utilizar una instrucción SQL SELECT para crear un cursor suena muy prometedor. Sin mucha dificultad, puede escribir un SELECT que represente las opciones de los usuarios, y que genera un cursor, que puede ser utilizado como el RecordSource del Grid.

Vamos a asumir que hemos configurado las siguientes variables:

  • lcCat contiene la categoría requerida.
  • llEnglish es .T. si el usuario escoge English como el lenguaje para la descripción del producto (en cuyo caso vamos a utilizar el campo eng_name como la segunda columna). Es .F. si el usuario desea verlo en el lenguaje original (para lo cual va a utilizar en su lugar el campo prod_name).
  • lcOrder contiene el número de la columna por la que se ordenará el dato (se almacena como cadena de caracteres).

El código en el botón Refresh puede tener este aspecto:

SELECT product_id,; 
  IIF(llEnglish,eng_name,prod_name) AS descript,;
  unit_price, in_stock ;
  FROM Products ;
  WHERE ALLTRIM(Category) = ALLTRIM(lcCat) ; 
  ORDER BY &lcOrder INTO CURSOR csrTemp
THISFORM.refresh

La instrucción SELECT envía el dato requerido al cursor, csrTemp. Este es el RecordSource para el Grid, entonces después que el formulario fue refrescado, el Grid  debe mostrar exactamente el dato que necesita el usuario. Problema solucionado.

No es tan sencillo

Desafortunadamente, no es tan sencillo. Si va a crear este formulario y ejecutarlo, el SELECT debía traer el dato correcto; pero el Grid aparecería como un rectángulo vacío. No se verían los datos.

La razón para este comportamiento no es difícil de ver. Siempre que utilice SELECT para crear un cursor de esta forma, Visual FoxPro destruye primero el cursor existente (si existe), luego, construye completamente uno nuevo. El Grid se desestabiliza con esto, ya que no desea perder el RecordSource, ni siquiera por un pequeño instante. Debido a que los controles dentro del Grid, están enlazados al cursor, destruyendo el cursor se destruyen los controles dentro del Grid, por eso se ve el rectángulo vacío.

Existirá alguna diferencia si utilizamos una tabla física en lugar de un cursor para la salida del SELECT? No, no habrá diferencia alguna.

La solución

Sin embargo, una vez que se entiende lo que ocurre, no es difícil idear una solución. El truco es crear un segundo cursor como el RecordSource, y mover los datos desde el primer cursor (aquel creado por el SELECT) al segundo cursor, el que el usuario desea actualizar en el Grid.

Vamos a colocar el siguiente código en el evento Load del formulario:

CREATE CURSOR csrProducts ; 
  ( product_id C(6), descript C(40), ;
  unit_price N(6,2), in_stock N(6) )

Esto va a crear un cursor, llamado crsProducts, con la misma estructura que el generado por SELECT. Establezca este cursor como RecordSource del Grid.

En el botón Refresh, mantenga el SELECT tal y como lo tenía antes; pero agregue algo de código para copiar el contenido del cursor generado por ese SELECT (csrTemp) en un cursor nuevo (csrProducts). El código entonces sería así:

SELECT product_id, ;
  IIF(llEnglish,eng_name,prod_name) AS descript,;
  unit_price, in_stock ;
  FROM Products ;
  WHERE ALLTRIM(Category) = ALLTRIM(lcCat) ; 
  ORDER BY &lcOrder INTO CURSOR csrTemp
SELECT csrProducts 
ZAP 
APPEND FROM DBF("csrTemp")
THISFORM.refresh

El efecto de esto es copiar los resultados del SELECT en csrProducts. Después que se ha refrescado el formulario, el dato se mostrará correctamente en el Grid.

Vea que no puede utilizar el comando COPY TO para transferir los datos a csrProducts, debido a que ese comando crea un archivo nuevo. En su lugar, necesita limpiar (ZAP) el contenido existente de csrProducts y agregar los datos nuevos. Observe además, el uso de la función DBF(). Esto es necesario debido a que el comando APPEND FROM puede solamente copiar datos desde una tabla física. DBF() devuelve la ruta y el nombre del archivo real que guarda el cursor.

Un detalle final al que debe prestar atención. Probablemente desee que el Grid muestre algún dato inicialmente cuando aparece el formulario por primera vez. Este dato debe basarse en los valores predeterminados para las tres selecciones del usuario. Para lograr esto, simplemente agregue código al Init del formulario para hacer el Select y para abrir los resultados en csrProducts. Por supuesto, va a necesitar además código para configurar las variables utilizadas en el SELECT (lcCat, llEnglish and lcOrder),  pero dejaremos esto como ejercicio para el lector.

Agradecimientos

La técnica que se ha descrito está basada en parte en la información del excelente libro "1001 Things You Always Wanted to Know About Visual FoxPro", por Marcia Akins, Andy Kramek y Rick Schummer (Hentzenwerke, 2000). Puede ver un resumen de este libro en nuestra página dedicada a libros: http://www.ml-consult.demon.co.uk/MLCBooks.htm o buscar más información en Amazon.com.

Mike Lewis Consultants Ltd. Septiembre 2001

13 de diciembre de 2015

Hacer que las Filas (registros) de un Grid sean de sólo lectura según condición

Si utilizas Grids para captura en linea, quizás quieras limitar que ciertas filas no puedan ser modificadas, es decir, dejarlas como de sólo lectura....

Quizás te resulte algo truculento, pero en realidad sirve... Puedes hacer a TODO el grid de sólo lectura, y cuando se cambie de registro, poner en .T. o .F. el valor de la propiedad ReadOnly (del Grid) según sea tu condición...

Veamos un ejemplo:

public oForm
oForm = CREATEOBJECT("MyForm")
oForm.Show()
DEFINE CLASS MyForm AS Form
   ADD OBJECT MyGrid AS Grid
   PROCEDURE LOAD
       CREATE CURSOR Temp (nMes int,cMes c(15))
       RAND(-1)
       FOR lnCounter=1 TO 20
         lnMes = RAND()*11+1
        INSERT INTO temp VALUES(lnMes,cMONTH(DATE(2003,lnMes,01)))
       ENDFOR
   ENDPROC
   PROCEDURE INIT
      WITH This.MyGrid 
         .SetAll("DynamicBackColor", ;
                 "IIF(RECNO()%2 =0,RGB(255,255,255), ;
                                   RGB(0,255,0))",;
                 "Column")
      EndWith
   ENDPROC
   PROCEDURE UNLOAD
      USE IN SELECT("Temp")
   ENDPROC
   PROCEDURE MyGrid.AfterRowColChange
   LPARAMETERS nColIndex
      This.ReadOnly=(RECNO()%2 # 0)
   ENDPROC   
ENDDEFINE   

Copia y Pega el código anterior en tu Command Window, selecciónalo y presiona ENTER, se ejecutará un formulario con un grid conteniendo valores obtenidos de una tabla llenada aleatoriamente, dicho grid le he pintado las filas impares (según el RECNO()), estas mismas líneas serán las que permanezcan de modo solo lectura, ¿Cómo realizaremos esto?, sencillo, en el método AfterRowColChange, si la fila actual es impar o no, pongo el valor de la propiedad ReadOnly a verdadero (.T.) o falso (.F.) según sea el caso.

Si, puede que sea muy "chapucero", pero funciona ;-), el chiste está en que debes establecer correctamente tu condicion de "Solo lectura" y hacerlo por medio del evento AfterRowColChange.

Este truco, se lo debemos a Drew Speddie!. Espero les sea de utilidad.

Espero que esta información les sea de utilidad.

Espartaco Palma Martínez

9 de diciembre de 2015

Convertir imágenes a distintos tipos de archivo

Una función que permite convertir imágenes a distintos formatos y calidad utilizando WIA (Windows Image Acquisition Automation Layer).

En este ejemplo solo utiliza el filtro de Conversión (Formato y Calidad). Pueden ver mas ejemplos de los distintos usos de filtros en la siguiente página: How to Use Filters.

* Ejemplo:
lcFile = GETPICT()
lcFileJPG = ConvertImageType(lcFile, "JPG", 90)
lcFileBMP = ConvertImageType(lcFile, "BMP")
lcFileGIF = ConvertImageType(lcFile, "GIF")
lcFilePNG = ConvertImageType(lcFile, "PNG")
lcFileTIF = ConvertImageType(lcFile, "TIF")
FUNCTION ConvertImageType(tcFileName, tcImageType, tnQuality)
  IF EMPTY(tcFileName) OR ;
      VARTYPE(tcFileName) <> "C" OR ;
      NOT FILE(tcFileName)
    RETURN .F.
  ENDIF

  IF EMPTY(tcImageType) OR ;
      VARTYPE(tcImageType) <> "C"
    tcImageType = "JPG"
  ENDIF

  IF EMPTY(tnQuality) OR ;
      VARTYPE(tnQuality) <> "N" OR ;
      BETWEEN(tnQuality,0,100)
    tnQuality = 100
  ENDIF

  LOCAL loImgFile, loImgProcess
  loImgFile = CREATEOBJECT("WIA.ImageFile")
  loImgProcess = CREATEOBJECT("WIA.ImageProcess")

  * Cargo la imagen a convertir
  loImgFile.LoadFile(tcFilename)

  * Agrego filtros
  loImgProcess.Filters.ADD(loImgProcess.FilterInfos("Convert").FilterID)

  * Nuevo tipo de archivo
  LOCAL lcFormatId
  DO CASE
    CASE tcImageType = "JPG"
      lcFormatID = "{B96B3CAE-0728-11D3-9D7B-0000F81EF32E}"
    CASE tcImageType = "BMP"
      lcFormatID = "{B96B3CAB-0728-11D3-9D7B-0000F81EF32E}"
    CASE tcImageType = "GIF"
      lcFormatID = "{B96B3CB0-0728-11D3-9D7B-0000F81EF32E}"
    CASE tcImageType = "PNG"
      lcFormatID = "{B96B3CAF-0728-11D3-9D7B-0000F81EF32E}"
    CASE tcImageType = "TIF"
      lcFormatID = "{B96B3CB1-0728-11D3-9D7B-0000F81EF32E}"
    OTHERWISE
      tcImageType = "JPG"
      lcFormatID = "{B96B3CAE-0728-11D3-9D7B-0000F81EF32E}"
  ENDCASE
  loImgProcess.Filters(1).Properties("FormatID").VALUE = lcFormatId

  * Calidad
  loImgProcess.Filters(1).Properties("Quality").VALUE = tnQuality && Calidad

  * Aplico filtros
  loImgFile  = loImgProcess.APPLY(loImgFile)

  LOCAL lcNewFileName
  lcNewFileName =   FORCEEXT(JUSTSTEM(tcFileName), tcImageType)

  * Que no exista el nombre de archivo
  LOCAL lnCount
  lnCount = 1
  DO WHILE FILE(lcNewFileName)
    lcNewFileName = FORCEEXT(JUSTSTEM(tcFileName) + TRANSFORM(lnCount), tcImageType)
    lnCount = lnCount + 1
  ENDDO

  * Guardo imagen procesada
  loImgFile.SaveFile(lcNewFileName)

  STORE NULL TO loImgFile, loImgProcess
  
  *Retorno el nuevo nombre de archivo convertido
  RETURN lcNewFileName
ENDFUNC

Luis María Guayán

Nota: Un reconocimiento al Blog de Jose Guillermo Ortiz Hernandez del cual tome información que me ayudo en la elaboración de esta función que cubre mis necesidades.

5 de diciembre de 2015

Saber la aplicación asociada a una extensión de archivo

Esta es la función en VFP:

FUNCTION AplicAsoc(tcExt)

Esta función retorna el nombre de la aplicación asociada a una extensión pasada como parámetro.

EJEMPLO:

? AplicAsoc("XLS")
EXCEL.EXE

Si la extensión pasada como parámetro no está asociada a ninguna aplicación, la función retorna una cadena vacia.

FUNCTION AplicAsoc(tcExt)
 LOCAL lcArc, lcApp, ln, ll, lc
 DECLARE LONG FindExecutable ;
  IN SHELL32.DLL ;
  STRING lpfile, ;
  STRING lpdirectory, ;
  STRING lpresult
 lcArc = FORCEEXT(SYS(5)+CURDIR()+SYS(2015),tcExt)
 ln = FCREATE(lcArc)
 ll = FCLOSE(ln)
 lc = SPACE(255)
 lcApp = ""
 IF FindExecutable(lcArc,"",@lc) >= 32
  lcApp = JUSTFNAME(SUBSTR(lc,1,AT(CHR(0),lc)-1))
 ENDIF
 IF FILE(lcArc)
  DELETE FILE (lcArc)
 ENDIF 
 RETURN lcApp
ENDFUNC

JMatheus

1 de diciembre de 2015

Capturando imágenes desde una cámara web

Código de ejemplo para capturar una imagen desde la cámara web

LOCAL oForm
oForm = CREATEOBJECT("Tform")
oForm.VISIBLE = .T.
READ EVENTS

DEFINE CLASS Tform AS FORM
  WIDTH = 760
  HEIGHT = 500
  AUTOCENTER = .T.
  CAPTION = "Using Video Capture"
  MINBUTTON = .F.
  MAXBUTTON = .F.
  SHOWWINDOW = 2
  BORDERSTYLE = 2
  SHOWTIPS = .T.

  ADD OBJECT cmdClose AS COMMANDBUTTON WITH CANCEL = .T.,;
    LEFT = 10, TOP = 150, HEIGHT = 27, WIDTH = 100, CAPTION = "Close"

  ADD OBJECT cmdGetFrame AS COMMANDBUTTON WITH;
    LEFT = 10, TOP = 5, HEIGHT = 27, WIDTH = 100, CAPTION = "Get Frame",;
    ENABLED = .F., TOOLTIPTEXT = "Updates the frame"

  ADD OBJECT cmdPreview AS COMMANDBUTTON WITH DEFAULT = .T.,;
    LEFT = 10, TOP = 33, HEIGHT = 27, WIDTH = 100, CAPTION = "Preview Video",;
    ENABLED = .F., TOOLTIPTEXT = "Turns preview mode on"

  ADD OBJECT cmdSave AS COMMANDBUTTON WITH LEFT = 10, TOP = 61,;
    HEIGHT = 27, WIDTH = 100, CAPTION = "Save to BMP",;
    TOOLTIPTEXT = "Saves current frame to BMP file"

  ADD OBJECT cmdFormat AS COMMANDBUTTON WITH LEFT = 10, TOP = 100,;
    HEIGHT = 27, WIDTH = 100, CAPTION = "Format",;
    TOOLTIPTEXT = "Displays available formats"

  ADD OBJECT capwindow AS TCaptureWindow

  PROCEDURE INIT
    =BINDEVENT(THIS.capwindow, "ResizeCaptureWindow",THIS, "OnCaptureWindowResized", 1)
  ENDPROC

  PROCEDURE ACTIVATE
    IF THIS.capwindow.hWindow = 0
      IF THIS.capwindow.InitCaptureWindow(THIS.HWND, 120, 5)
        STORE .T. TO THIS.cmdGetFrame.ENABLED,;
          THIS.cmdPreview.ENABLED
        THISFORM.capwindow.StartPreview
      ENDIF
    ENDIF
  ENDPROC

  PROCEDURE DESTROY
    CLEAR EVENTS
  ENDPROC

  PROCEDURE cmdClose.CLICK
    THISFORM.RELEASE
  ENDPROC

  PROCEDURE cmdGetFrame.CLICK
    THISFORM.capwindow.GetFrame
  ENDPROC

  PROCEDURE cmdPreview.CLICK
    THISFORM.capwindow.StartPreview
  ENDPROC

  PROCEDURE cmdFormat.CLICK
    THISFORM.capwindow.FormatDlg
  ENDPROC

  PROCEDURE cmdSave.CLICK
    THISFORM.capwindow.SaveToDib
  ENDPROC

  PROCEDURE OnCaptureWindowResized
    WITH THIS.capwindow
      IF .capWidth = 0 OR .capHeight = 0
        RETURN
      ENDIF
      THIS.WIDTH = MAX(320, .capLeft+.capWidth+5)
      THIS.HEIGHT = MAX(240, .capTop+.capHeight+25)
      THIS.cmdClose.TOP = THIS.HEIGHT-60
    ENDWITH
  ENDPROC

ENDDEFINE

DEFINE CLASS TCaptureWindow AS CUSTOM
  #DEFINE WM_CAP_START  0x0400
  #DEFINE WM_CAP_DRIVER_CONNECT    (WM_CAP_START+10)
  #DEFINE WM_CAP_DRIVER_DISCONNECT (WM_CAP_START+11)
  #DEFINE WM_CAP_DRIVER_GET_CAPS   (WM_CAP_START+14)
  #DEFINE WM_CAP_FILE_SAVEDIB      (WM_CAP_START+25)
  #DEFINE WM_CAP_DLG_VIDEOFORMAT   (WM_CAP_START+41)
  #DEFINE WM_CAP_GET_VIDEOFORMAT   (WM_CAP_START+44)
  #DEFINE WM_CAP_SET_VIDEOFORMAT   (WM_CAP_START+45)
  #DEFINE WM_CAP_SET_PREVIEW       (WM_CAP_START+50)
  #DEFINE WM_CAP_SET_OVERLAY       (WM_CAP_START+51)
  #DEFINE WM_CAP_SET_PREVIEWRATE   (WM_CAP_START+52)
  #DEFINE WM_CAP_SET_SCALE         (WM_CAP_START+53)
  #DEFINE WM_CAP_GET_STATUS        (WM_CAP_START+54)
  #DEFINE WM_CAP_GRAB_FRAME        (WM_CAP_START+60)

  #DEFINE WS_CHILD 0x40000000
  #DEFINE WS_VISIBLE 0x10000000
  #DEFINE SWP_SHOWWINDOW 0x40
  #DEFINE BITMAPINFOHEADER_SIZE 40
  #DEFINE CAPDRIVERCAPS_SIZE 44

  hWindow = 0
  hCapture = 0
  capWidth = 0
  capHeight = 0
  capOverlay = 0
  capLeft = 0
  capTop = 0

  PROCEDURE INIT
    THIS.DECLARE
  ENDPROC

  PROCEDURE DESTROY
    THIS.ReleaseCaptureWindow
  ENDPROC

  PROCEDURE InitCaptureWindow(hParent, nLeft, nTop)
    WITH THIS
      .hWindow = m.hParent
      .capLeft = m.nLeft
      .capTop = m.nTop
      STORE 0 TO .capWidth, .capHeight

      .hCapture = capCreateCaptureWindow("",;
        BITOR(WS_CHILD,WS_VISIBLE), .capLeft, .capTop,;
        1,1, .hWindow, 1)

      IF .DriverConnect()
        .msg(WM_CAP_SET_SCALE, 1, 0)
        .ResizeCaptureWindow
      ENDIF
    ENDWITH
    RETURN THIS.IsCaptureConnected()
  ENDPROC

  PROCEDURE msg(msg, wParam, LPARAM, nMode)
    DO CASE
      CASE THIS.hCapture = 0
      CASE VARTYPE(nMode) <> "N" OR nMode = 0
        =SendMsgInt(THIS.hCapture, msg, wParam, LPARAM)
      OTHERWISE
        =SendMsgStr(THIS.hCapture, msg, wParam, @LPARAM)
    ENDCASE
  ENDPROC

  PROCEDURE ResizeCaptureWindow
    THIS.GetVideoFormat
    =SetWindowPos(THIS.hCapture, 0, THIS.capLeft,THIS.capTop,;
      THIS.capWidth, THIS.capHeight, SWP_SHOWWINDOW)
  ENDPROC

  PROCEDURE DriverConnect
    THIS.msg(WM_CAP_DRIVER_CONNECT, 0,0)
    IF THIS.IsCaptureConnected()
      RETURN .T.
    ELSE
      RETURN .F.
    ENDIF
  ENDPROC

  PROCEDURE DriverDisconnect
    THIS.msg(WM_CAP_DRIVER_DISCONNECT, 0,0)
  ENDPROC

  PROCEDURE ReleaseCaptureWindow
    IF THIS.hCapture <> 0
      THIS.DriverDisconnect
      = DestroyWindow(THIS.hCapture)
      THIS.hCapture = 0
    ENDIF
  ENDPROC

  PROCEDURE GetFrame
    THIS.msg(WM_CAP_GRAB_FRAME, 0,0)
  ENDPROC

  PROCEDURE GetVideoFormat
    LOCAL cBuffer, nBufsize
    nBufsize = 4096
    cBuffer = PADR(THIS.num2dword(BITMAPINFOHEADER_SIZE), nBufsize, CHR(0))
    THIS.msg(WM_CAP_GET_VIDEOFORMAT, nBufsize, @cBuffer, 1)
    THIS.capWidth = THIS.buf2dword(SUBSTR(cBuffer, 5,4))
    THIS.capHeight = THIS.buf2dword(SUBSTR(cBuffer, 9,4))
  ENDPROC

  PROCEDURE FormatDlg
    THIS.msg(WM_CAP_DLG_VIDEOFORMAT, 0,0)
    THIS.ResizeCaptureWindow
  ENDPROC

  FUNCTION IsCaptureConnected
    LOCAL cBuffer, nResult
    cBuffer = REPLI(CHR(0),CAPDRIVERCAPS_SIZE)
    THIS.msg(WM_CAP_DRIVER_GET_CAPS, LEN(cBuffer), @cBuffer, 1)
    THIS.capOverlay = THIS.buf2dword(SUBSTR(cBuffer,5,4))
    nResult = ASC(SUBSTR(cBuffer, 21,1))
    RETURN (nResult <> 0)
  ENDPROC

  PROCEDURE StartPreview
    THIS.msg(WM_CAP_SET_PREVIEWRATE,30,0)
    THIS.msg(WM_CAP_SET_PREVIEW, 1,0)
    IF THIS.capOverlay <> 0
      THIS.msg(WM_CAP_SET_OVERLAY,1,0)
    ENDIF
  ENDPROC

  PROCEDURE StopPreview
    THIS.msg(WM_CAP_SET_PREVIEW, 0,0)
  ENDPROC

  PROCEDURE SaveToDib
    LOCAL cFilename
    cFilename = "pic" + SYS(2015) + ".bmp" + CHR(0)
    THIS.msg(WM_CAP_FILE_SAVEDIB, 0, @cFilename, 1)
  ENDPROC

  PROCEDURE DECLARE
    DECLARE INTEGER DestroyWindow IN user32 INTEGER hWindow

    DECLARE INTEGER capCreateCaptureWindow IN avicap32;
      STRING lpszWindowName, LONG dwStyle,;
      INTEGER x, INTEGER Y, INTEGER nWidth,;
      INTEGER nHeight, INTEGER hParent, INTEGER nID

    DECLARE INTEGER SetWindowPos IN user32;
      INTEGER hWindow, INTEGER hWndInsertAfter,;
      INTEGER x, INTEGER Y, INTEGER cx, INTEGER cy,;
      INTEGER wFlags

    DECLARE INTEGER SendMessage IN user32 AS SendMsgInt;
      INTEGER hWindow, INTEGER Msg,;
      INTEGER wParam, INTEGER LPARAM

    DECLARE INTEGER SendMessage IN user32 AS SendMsgStr;
      INTEGER hWindow, INTEGER Msg,;
      INTEGER wParam, STRING @LPARAM
  ENDPROC

  PROCEDURE buf2dword(lcBuffer)
    RETURN ASC(SUBSTR(lcBuffer, 1,1)) + ;
      BITLSHIFT(ASC(SUBSTR(lcBuffer, 2,1)),  8) +;
      BITLSHIFT(ASC(SUBSTR(lcBuffer, 3,1)), 16) +;
      BITLSHIFT(ASC(SUBSTR(lcBuffer, 4,1)), 24)
  ENDPROC

  PROCEDURE num2dword(lnValue)
    #DEFINE m0 0x100
    #DEFINE m1 0x10000
    #DEFINE m2 0x1000000
    IF lnValue < 0
      lnValue = 0x100000000 + lnValue
    ENDIF
    LOCAL b0, b1, b2, b3
    b3 = INT(lnValue/m2)
    b2 = INT((lnValue - b3*m2)/m1)
    b1 = INT((lnValue - b3*m2 - b2*m1)/m0)
    b0 = MOD(lnValue, m0)
    RETURN CHR(b0)+CHR(b1)+CHR(b2)+CHR(b3)
  ENDPROC

ENDDEFINE

23 de noviembre de 2015

Usando la Nueva clase Empty de VFP 8

Este es un Ejemplo de Como Aprovechar la Nueva Clase "Empty" que nos proporciona VFP8.

Esta Rutina, nos permite Leer Los Nombres de las variables que usaremos en Nuestro Programa, en una Tabla y su Respectivo Valor, para No Tener que recompilar el programa cada vez que tengamos que cambiar el valor de una variable.

En Lugar de Variables Publicas, podemos tener un Objeto Global, que contendra una propiedad por cada Registro que encuentre en la tabla, y su valor correspondiente.

La Ventaja es que el objeto solo contendra propiedades, ningun metodo o evento, por lo tanto sera mas que liviano. y pueden ser facilmente pasado como parametro a otras aplicaciones nuestras variables.

El Codigo Es Este:

Define Class oEntorno As Session
  Procedure LeerConf
  Lparameters cArchivo, cCampo, cValor
  If Type('cArchivo')<>'C' Or Type('cCampo')<>'C' Or Type('cValor')<>'C' Or ;
    Len(Alltrim(cArchivo))= 0 Or Len(Alltrim(cCampo))= 0 Or Len(Alltrim(cValor))= 0
    Wait Window 'Error del Programador!'
    Return .F.
  Endif
  Local uValor, cCad, nCiclo, oProp, cAlias
  Use In Select('Origen')
  Use (cArchivo) Alias Origen Again In 0 Shared
  cCad=',.*+/- =()[]{}<>^&%$#"!@)(¿?'+"'"
  Select Distinct (&cCampo) As cOpcion, (&cValor) As uValor From Origen Into Cursor CurProp Readwrite
  If Reccount('CurProp')>0
    Update CurProp Set cOpcion =Chrtran(Alltrim(CurProp.cOpcion), cCad, Replicate('_',Len(cCad)))
    oProp = Createobject("Empty")
    Select CurProp
    Scan
      AddProperty(oProp, Alltrim(CurProp.cOpcion),'')
      Do Case
      Case Alltrim(Upper(transform(curprop.uValor)))='.NULL'
        cCad = [Store .Null. to Oprop.]+Alltrim(CurProp.cOpcion)
      Case Type(Evaluate("curprop.uValor"))='L'
        cCad = [Store ]+ Upper(Transform(CurProp.uValor))+[ to Oprop.]+Alltrim(CurProp.cOpcion)
      Case Getwordcount(CurProp.uValor,'/-')=3 And Len(Alltrim(CurProp.uValor))=10
        cCad = [Store {^] +Alltrim(CurProp.uValor)+ [} to Oprop.]+Alltrim(CurProp.cOpcion)
      Case Type(Evaluate([curprop.uValor]))='N'
        cCad = [Store ]+ Upper(Transform(CurProp.uValor))+[ to Oprop.]+Alltrim(CurProp.cOpcion)
      Otherwise
        cCad = [Store "]+ Upper(Transform(CurProp.uValor))+[" to Oprop.]+Alltrim(CurProp.cOpcion)
      Endcase
      &cCad
    Endscan
  Endif
  Use In Select('CurProp')
  Use In Select('Origen')
  Return oProp
  Endproc
Enddefine

Los parametros que deben pasarse son:

  • La ruta al Archivo que leeremos
  • El Nombre del Campo que contendra los nombres de las propiedades
  • El Nombre del Campo que contendra los valores

Para Usarlo, suponiendo que Nuestra Tabla es: "C:\configuravariables.dbf" y la estructura es:

Id Numeric(2), Nombre char(16), Valor Char(200)

Hariamos

Set Library to oEntorno.prg  && donde oEntorno.prg es el nombre con que grabaron esta rutina
Local oEnt
Public oGlobal
oEnt = CreateObject("oEntorno")
oGlobal = oEnt.LeerConf("C:\configuravariables.dbf","Nombre","Valor")

y a partir de este momento, oGlobal ya tiene tantas propiedades como registros en la tabla.

NOTA: Ambos campos tienen que ser Caracter. La rutina intenta detectar el tipo de dato que es, para las variables de tipo Date, debe ir almacenada en el formato AAAA/MM/DD. Acepta registros con campo =Null para los valores.

Saludos desde Guatemala.

Jorge Mota

18 de noviembre de 2015

Conocer si un DLL esta registrada antes de Instanciarla

Una forma de evitar que un error se produzca si tu DLL o Servidor COM no estuviara registrado en la PC de producción.

Suele suceder que si algún componente externo de tu aplicación no este registrado y al momento de querer instanciarlo via CREATEOBJECT(), arrojandonos el Error #1733 "No se encuentra la definición de clase ..."

El codigo para evitarlo es relativamente sencillo. Utilizando la clase Registry que está incluido en Visual FoxPro dentro de las Fox Foundation Classes (FFCs).

loRegistry = NEWOBJECT("Registry",HOME(1)+"ffc\registry.vcx")
IF loRegistry.Iskey("zipit.cgzipfiles")
   *** Hacer lo propio...
ELSE
    Messagebox("No está registrado el componente de Compresión")
END

Espero les sea de utilidad.

Espartaco Palma Martínez

16 de noviembre de 2015

Trucos de WSH

Siempre es interesante conocer algunas de las funciones del WSH, esto brinda un poco mas de dinamica a nuestras aplicaciones.

OBTENER NOMBRE DEL PC, DOMINIO Y USUARIO
WshNetwork = CreateObject('WScript.Network')
lcMessage='Domain = ' + WshNetwork.UserDomain + CHR(13)
lcMessage=lcMessage+ 'Computer Name =' + WshNetwork.ComputerName+CHR(13)
lcMessage=lcMessage+ 'User Name = ' + WshNetwork.UserName
MESSAGEBOX(lcMessage)
OBTENER INFORMACION SOBRE LAS UNIDADES DE CD
LOCAL strComputer
Local lcString
strComputer = '.'
lcString = ''
objWMIService = Getobject('winmgmts:'+ 'impersonationLevel=impersonate}!\\' + strComputer + '\root\cimv2')
colItems = objWMIService.ExecQuery('Select * from Win32_CDROMDrive')
For Each objItem In colItems
    lcString = lcString + 'Description: '+objItem.Description+Chr(13)
    lcString = lcString + 'Name: '+objItem.Name+Chr(13)
    lcString = lcString + 'Manufacturer:' +objItem.manufacturer+Chr(13)
    lcString = lcString + 'Media type: '+objItem.mediaType+Chr(13)
    lcString = lcString + 'PNP Device ID:' + objItem.PNPDeviceID +Chr(13)
Next
Messagebox(lcString)
MAPEAR UNA UNIDAD DE RED
oNet = CreateObject('WScript.Network')    
oNet.MapNetworkDrive('I','\\myserver\myFiles',.T.,'mike','password')
DESCONECTAR DE UNA UNIDAD DE RED
WshNetwork = CreateObject('WScript.Network')
WshNetwork.RemoveNetworkDrive('E')
CONFIGURAR UNA IMPRESORA POR DEFAUL
oNet = CreateObject('WScript.Network')    
oNet.SetDefaultPrinter('\\ServerName\PrinterName')
OBTENER EL ESPACIO LIBRE EN DISCO
objFSO = CreateObject('Scripting.FileSystemObject')
objDrive = objFSO.GetDrive('C:')
MESSAGEBOX('Available space: ' + chr(13)+TRANSFORM(objDrive.AvailableSpace,'999,999,999,999,999'+' kb' ))
COMO COPIAR UN ARCHIVO DEUN LUGAR A OTRO
FSO = CreateObject('Scripting.FileSystemObject')
FSO.CopyFile('c:\COMPlusLog.txt','c:\x\')
COMO CREAR UNA CARPETA
fso = createobject('Scripting.FileSystemObject')
fldr = fso.CreateFolder('C:\MyTest')
COMO BORRAR UNA CARPETA
fso =createobject('Scripting.FileSystemObject')
fldr = fso.DeleteFolder('C:\MyTest')
DETERMINAR SI UNA CARPETA EXISTE
fso =createobject('Scripting.FileSystemObject')
? fso.FolderExists('C:\MyTest')
COMO CREAR UN ARCHIVO
fso = CreateObject('Scripting.FileSystemObject')
f1 = fso.CreateTextFile('c:\testfile.txt', .T.)
COMO CREAR UN ARCHIVO Y ESCRIBIR EN EL
fso = CreateObject('Scripting.FileSystemObject')
tf = fso.CreateTextFile('c:\testfile.txt', .t.)
tf.WriteLine('Testing 1, 2, 3.') 
tf.WriteBlankLines(3) && Skip three lines
tf.Write ('This is a test.') 
tf.Close
MODIFY FILE 'c:\testfile.txt'
COMO CREAR UN ICINO EN EL ESCRITORIO
Shell = CreateObject('WScript.Shell')
DesktopPath = Shell.SpecialFolders('Desktop')
link = Shell.CreateShortcut(DesktopPath+'\test.lnk')
link.Arguments = '1 2 3'
link.Description = 'test shortcut'
link.HotKey = 'CTRL+ALT+SHIFT+X'
link.IconLocation = 'app.exe,1'
link.TargetPath = 'c:\blah\app.exe'
link.WindowStyle = 3
link.WorkingDirectory = 'c:\blah'
link.Save()
COMO CREAR UNA ENTRADA EN EL REGISTRY DE WINDOWS
oSh = CreateObject('WScript.Shell')
key =  'HKEY_CURRENT_USER\'
oSh.RegWrite( key + 'WSHTest\','testkeydefault')
oSh.RegWrite(key + 'WSHTest\string1', 'testkeystring1')
oSh.RegWrite( key + 'WSHTest\string2', 'testkeystring2', 'REG_SZ')
oSh.RegWrite( key + 'WSHTest\string3', 'testkeystring3', 'REG_EXPAND_SZ')
oSh.RegWrite( key + 'WSHTest\int', 123, 'REG_DWORD')
COMO REMOVER LA ENTRADA
oSh = CreateObject('WScript.Shell')
oSh.RegDelete('HKCU\\Software\\ACME\\FortuneTeller\\MindReader')
oSh.RegDelete('HKCU\\Software\\ACME\\FortuneTeller\\')
oSh.RegDelete ('HKCU\\Software\\ACME\\')

Se que les seran de alguna utilidad.

Disfrutenlas.

Mauricio Henao Romero

11 de noviembre de 2015

Buffer y bloqueos en Visual FoxPro - Resumen

Artículo original: Buffering and Locking in Visual FoxPro - an Overview
http://weblogs.foxite.com/andykramek/archive/2005/10/18/948.aspx
Autor: Andy Kramek
Traducido por: Ana María Bisbé York


¿Dónde comenzamos?

Parece que trabajar con datos en buffer en Visual FoxPro causa mucha confusión. Creo que es en gran medida debido a la confusión en la implementación del buffer y en parte también, por problemas de nomenclatura (por estándares convencionales) asociados al tema.

Por ejemplo, para definir buffer para un archivo DBF de Visual FoxPro, que es una tabla, tenemos que utilizar la función CURSORSETPROP(), que está excesivamente sobrecargada. ¿Por qué no tener una función separada, sin ambigüedades, " SetBufferMode()"? Para confirmar una transacción pendiente el comando es END TRANSACTION. ¿Por qué no "COMMIT" como en el resto de los lenguajes de bases de datos - una opción que incluso es más peculiar ya que el estándar "ROLLBACK" se utiliza para revertir una transacción?

Además, quizás debido a que Visual FoxPro implementa el bloqueo de los registros (lo contrario a "Página"), el hecho de controlar la colocación y liberación de los bloqueos está inseparablemente enlazada con el buffer. Por ejemplo, para permitir buffer de filas, (que solamente opera sobre un sólo registro) SET MULTILOCKS debe estar en "ON". De acuerdo con el archivo Ayuda, esta configuración solamente determina la capacidad de Visual FoxPro para bloquear múltiples registros en la misma tabla - y que de todas formas, está limitado a la actual sesión de datos. Esto no parece ser muy lógico y no es realmente sorprendente que la gente esté totalmente confusa.

¿Qué significa "buffer"?

El principio es realmente muy simple. Cuando hace cambios en algún dato, esos cambios no es escriben en la tabla fuente, en lugar van a un área de almacenaje (el "Buffer") hasta el momento en que le indica a Visual FoxPro que deben ser guardados en el almacén permanente o eliminarlos. Esta área de almacenaje es lo que, en la actualidad, ve en Visual FoxPro cuando se utiliza una tabla de buffer, en realidad un cursor actualizable basado en la tabla original. Todos los cambios se han hecho sobre este cursor y sólo se escriben en la tabla cuando se utiliza el comando "update" adecuado.

Estrategias de buffer

La estrategia de buffer determina cuándo y cómo se almacenan en la tabla los cambios que se encuentran en el buffer. Existen tres opciones:

  • No se emplea buffer: Esta vía es solamente una opción para las versiones anteriores a Visual Foxpro versión 3.0 y es además, el comportamiento predeterminado actualmente para las tablas de Visual FoxPro. Cualquier cambio hecho en una tabla va directamente e inmediatamente a la tabla original. No hay posibilidad de "deshacer" sin que se haya programado explícitamente - utilizando Scatter y Gather, por ejemplo. Se establece al asignar "1" como parámetro a CursorSetProp()
  • Buffer de filas: Los cambios no se han enviado a la tabla original a menos que ocurran una de estas dos cosas. O hay una llamada explícita a la función TableUpdate(), o el puntero del registro se mueve dentro de la tabla original. Vea que CUALQUIER movimiento del puntero del registro, aunque sea iniciado por una tabla que esté en buffer de filas, siempre causa un TableUpdate() "implícito". Establezca 2 ó 3 como parámetro para CursorSetProp().
  • Buffer de tabla: Los cambios nunca se envían automáticamente a la tabla original, a menos que exista una llamada a un comando TableUpdate() o TableRevert() siempre debe afectar los cambios que estén guardados en el buffer. Intentar cerrar una tabla con buffer mientras tiene cambios no cometidos provoca que Visual FoxPro genere un error en la versión 3.0; pero este comportamiento cambió en versiones anteriores por tanto, los cambios pendientes simplemente se pierden. (No hay error; ni advertencia de que los cambios se van a perder). Se establece asignando 4 ó 5 como parámetro para CursorSetProp().

Hasta este punto se está preguntando, por qué hay DOS parámetros posibles para cada una de las estrategias que implementan buffer. La respuesta es, como se indica en la introducción, debido a que hay dos estrategias de bloqueo.

Estrategias de bloqueo

Visual FoxPro necesita bloquear el registro físico en la tabla mientras se realizan los cambios en su contenido y existen dos vías con las que se puede establecer el bloqueo automático (opuesto al uso explícito de las funciones RLock()/FLock())

  • Bloqueo pesimista: El registro se bloquea en cuanto un usuario comienza a hacer cambios (El bloqueo en realidad ocurre en cuanto se oprime cualquier tecla válida). Esto evita que cualquier otro usuario pueda hacer o guardar cambios sobre este registro hasta tanto el usuario actual haya completado sus cambios y liberado el registro tanto guardando o revirtiendo los cambios.
  • Bloqueo optimista: Un intento de bloqueo del registro se hace solamente cuando los cambios se comienzan a enviar a la tabla. Esto significa que incluso mientras un usuario hace cambios en el dato, el registro permanece disponible a otros usuarios, los que también podrían, y posiblemente guardarán, cambios en el mismo registro mientras el primer usuario están aun trabajando en el.

Modos de buffer

El modo de buffer para una tabla es, por tanto, la combinación específica de las estrategias de Buffer y Bloqueo que se aplican. Existe un total de 5 modos de buffer para una tabla como se ilustra en la Tabla 1.

Tabla 1. Modos de Buffer en Visual FoxPro

ModoBloqueoBufferComentarios
1PesimistaNingunoÚnica opción para FP2.x, predeterminado para tablas VFP
2PesimistaFilaEl bloqueo se establece por el evento KeyPress. El movimiento del puntero de registro obliga a Guardar
3OptimistaFilaEl bloqueo se establece por TableUpdate(). El movimiento del puntero de registro obliga a Guardar
4PesimistaTablaEl bloqueo se establece por el evento KeyPress. Guardar se debe iniciar explícitamente
5OptimistaTablaEl bloqueo se establece por el evento TableUpdate(). Guardar se debe iniciar explícitamente

Al trabajar con Visual FoxPro, debemos ser cuidadosos al distinguir entre estrategias individuales, que se especifican directamente para Buffer y Bloqueo, y el modo buffer que resulta de la combinación de ellos. Desafortunadamente, como hemos visto, Visual FoxPro es de por sí, menos cuidadoso con esta distinción.

¿Qué significa todo esto cuando creamos formularios enlazados a datos?

He aquí donde las cosas comienzan a ponerse un poco más complejas (y no es solamente por la nomenclatura). Consideremos una situación normal donde las tablas se agregan al formulario por el entorno de datos nativo. El formulario tiene una propiedad llamada "Buffermode" que tiene tres valores posibles:

  • 0 Ninguno (predeterminado)
  • 1 Pesimista
  • 2 Optimista

Vea que esto se refiere en realidad a las opciones para la estrategia de bloqueo y no tiene nada que ver con el buffer ¡¡ para nada !! El hecho por el cual el formulario determina la estrategia buffer es por sus tablas todas por si mismo, basadas en su utilización. Si un formulario utiliza dos tablas que tienen una relación de uno a muchos, muestran el lado "muchos" de la relación de diferentes formas.

Si la tabla "muchos" se utiliza como origen de datos para el control grid, para el formulario, se abre con buffer de tabla. Sin embargo, si la tabla "muchos" se emplea para enlazar con controles que solamente muestran una fila de la tabla cada vez entonces, el buffer para la tabla va a ser lo mismo que para "una" tabla para toda de configuración de la propiedad Buffermode del formulario.

Si crea un pequeño formulario de prueba y ejecuta todas sus opciones para la propiedad Buffermode de un formulario, puede ver que incluso con la propiedad BufferMode establecida en 0-(Ninguna) el cursor creado por Visual FoxPro está aun abierto en modo buffer de fila cuando las tablas se abren desde el entorno de datos del formulario. Es como si para el formulario "No buffer" y "Bufer de filas" fuera lo mismo.

Sin embargo, este NO es el caso si las tablas se abren directamente con el comando USE. Si las tablas se abren explícitamente en el método Load(), entonces la propiedad Buffermode no tiene impacto en lo absoluto, y las tablas se abren en dependencia de los parámetros apropiados en la Sesión de datos. Para formularios que corren en la sesión predeterminada de datos (DataSession = 1) se utilizan los parámetros especificados en la ventana Opciones. Vea que en la ventana Opciones, las opciones, en realidad establecen la configuración del modo de bufer. Tiene las 5 opciones que se corresponden con lo 5 modos definidos antes, en la Tabla 1, y los que utilizan los mismos identificadores que la función CursorSetProp(). Si el formulario se ejecuta en sesión privada de datos, entonces, las tablas abiertas con el comando USE se configuran en dependencia de la configuración establecida para esa sesión de datos y, de forma predeterminada, no serán alojadas en buffer.

¿Aun confundido?

Lo más que puedo decir es que la situación es realmente como sigue:

  • Para las tablas abiertas por el entorno de datos del formulario, no importa si el formulario tiene Datassesion con valor Default o Private. Las tablas siempre tienen almacenamiento en buffer, al menos en modo Optimista de filas.
  • La propiedad BufferMode del formulario en realidad determina la estrategia de bloqueo, no el modo de buffer; pero solo para tablas que han sido abiertas en el entorno de datos del formulario.
  • En la sesión de datos actual, las tablas que han sido abiertas explícitamente tienen ambas estrategias: buffer y bloqueo establecidas en dependencia de la ventana Opciones.
  • En la sesión Privada de datos, las tablas que han sido abiertas explícitamente tienen sus estrategias de buffer y bloqueo establecidas de acuerdo a los parámetros que se han aplicado para esta sesión (Predeterminado = "No Buffering")
  • Estos resultados se pueden verificar estableciendo varias opciones en un formulario sencillo y mostrando el resultado que se obtiene por CURSORGETPROP("BUFFERING")

Entonces, ¿cómo debemos establecer el buffer para un formulario?

La respuesta corta , como siempre, es "depende". Si utiliza el entorno de datos del formulario para abrir las tablas entonces, puede normalmente dejar la opción al Visual FoxPro. En caso contrario puede simplemente emplear la función CursorSetProp() en su código para configurar cada tabla como sea necesario. De cualquier manera necesita ser consciente de las consecuencias para que pueda programar sus actualizaciones rápidamente.

Utilizar BufferModeOverride

La clase dataenvironment posee una propiedad para cada tabla (o, mejor dicho, "cursor") llamado "BufferModeOverride". Esta propiedad va a fijar el modo del buffer para esa tabla (y sólo esa tabla) a una de las estas seis opciones - si, es correcto, SEIS opciones, no cinco - veamos:

  • 0 Ninguno
  • 1 (Predeterminado) Utilizar la configuración del formulario.
  • 2 Búffer pesimista de fila
  • 3 Buffer optimista de fila
  • 4 Buffer pesimista de tabla
  • 5 Buffer optimista de tabla

Existen dos puntos sobre los que hay que prestar atención. Primero, Vea que mientras los números del 2 al 5 se corresponden con los parámetros de CursorSetProp() y son aquellos mismos que están disponibles desde el diálogo Opciones, el valor requerido para la configuración "ninguno" ahora es 0 en lugar de 1. He aquí otra inconsistencia en la configuración del buffer. Segundo, vea que el valor predeterminado para esta propiedad es "1 - Utiliza la configuración de formularios". Cuando se refiere a configuración de formularios, se refiere, por supuesto a la propiedad "BufferMode", la que como ya hemos visto en realidad se encarga de definir la estrategia de bloqueo a aplicar. No existe configuración del formulario para controlar buffer !!.

Habiendo dicho esto, la configuración de la propiedad BufferModeOverride va a asegurar que la tabla se abre utilizando el modo buffer que usted ha especificado. Al menos esta propiedad ha sido nombrada correctamente, sobreescribe todo lo demás y fuerza la tabla a un modo especial de buffer en todas las situaciones.

Utilizar CursorSetProp()

Independientemente de cómo fue abierta la tabla, siempre puede utilizar la función CursorSetProp() para cambiar el modo buffer de una tabla. Sin embargo, si está utilizando el entorno de datos del formulario para abrir sus tablas, entonces, tiene dos opciones en dependencia de si su formulario utiliza sesión privada de datos o sesión actual de datos. En el primer caso, puede establecer simplemente el modo requerido en la ventana Opciones y olvidarse de ello. Todas las tablas se van a abrir siempre con esa configuración y siempre sabrá en qué estado se encuentra. Si utiliza una sesión privada de datos entonces, necesita hacer dos cosas. Primero, necesita asegurarse de que el entorno de datos está configurado para soportar buffer. Algunos parámetros de entorno se limitan a la sesión de datos actual y puede que necesite cambiar el comportamiento predeterminado o alguno, o todos los siguientes (vea el tema SET DATASESSION en el archivo ayuda para una lista completa de parámetros afectados):

  • SET MULTILOCKS - Debe ser ON para permitir buffer, predeterminado OFF
  • SET DELETED - Predeterminado OFF
  • SET DATABASE - "No database" es el predeterminado en una sesión Privada
  • SET EXCLUSIVE - El predeterminado es OFF para una sesión Privada
  • SET LOCK - Predeterminado OFF
  • SET COLLATE - Predeterminado es "MACHINE"
  • SET EXACT - Predeterminado es OFF

Lo segundo que necesita es especificar explícitamente el modo buffer de cada tabla utilizada en la función CursorSetProp() con el parámetro apropiado porque la configuración en la ventana Opciones no se puede aplicar a la sesión privada de datos.

Entonces, ¿qué modo de buffer debo utilizar en mis formulario?

Para nosotros la respuesta es sencilla. Siempre debe utilizar estrategia de buffer de tabla con bloqueo optimista (es decir modo de buffer 5). La razón es simplemente que, con la excepción de creación de un índice, no hay nada que pueda hacer en otro modo que no pueda hacer en este modo. Mientras el buffer de filas puede ser utilizado en desarrollo, no creemos que tienen cabida en una aplicación en funcionamiento. La razón es simplemente que existe muchas formas en las que puede ser desencadenada la función TableUpdate() implícito (causado por el movimiento del puntero de registro) y no todos los que están por debajo de nuestro control directo. Por ejemplo, la función KeyMatch() está definida en el archivo Ayuda, como;

Busca una clave de índice en una etiqueta o un archivo de índice.

Parece suficientemente inofensivo - seguramente buscar un archivo índice no puede causar problemas. Pero una nota (en la sección Observaciones justo al final del tema) indica que:

KEYMATCH( ) devuelve el puntero de registro al registro donde estaba originalmente antes de ejecutar KEYMATCH( ).

Aquí se bloquea! Seguramente "devuelve el puntero de registro" implica que mueve el puntero del cursor - lo que en efecto - hace. La consecuencia es que si está utilizando el buffer de filas y quiere verificar por clave duplicados utilizando keymatch(), puede realizar inmediatamente cualquier cambio pendiente. (Por supuesto, en la versión 6 o después pueden siempre utilizar en su lugar IndexSeek(). Sin embargo, el mismo problema surge cuando muchos de los comandos y funciones que operan sobre una tabla - especialmente el más viejo introducido en FoxPro antes los días de buffer (e.g. CALCULATE, SUM y AVERAGE).

Aun más importante, el hecho de que configure una tabla para buffer de tabla no lo previene de que la tabla como si en realidad hubiera buffer de filas. Ambas funciones TableUpdate() y TableRevert(). Esto puede significar que tiene que escribir un poco más de código; pero puede evitar muchos problemas.

Cambiar el modo de buffer de la tabla

Hemos dicho, al inicio de la última sesión, que puede utilizar siempre CursorSetProp() para establecer o cambiar el modo de buffer de una tabla. Esto es cierto; pero si la tabla ya ha tenido asignado un modo buffer, puede no ser tan sencillo porque al cambiar el estado de buffer forzaría a Visual FoxPro a verificar el estado de cualquier buffer existente.

Si la tabla tiene buffer de filas, y tiene cambios no confirmados, Visual FoxPro los envía y permite cambiar el modo. Si el destino tiene buffer de tablas, y trata de cambiar su modo de buffer mientras existen cambios no confirmados, Visual FoxPro se queja y manda el error 1545 ("El búfer de tablas para el alias "nombre" contiene cambios no confirmados"). Esto es un problema porque no puede indexar una tabla, o hacerla libre una tabla o cursor transactable, los que mientras la tabla tiene buffer, por tanto es la única vía de hacer estas cosas, temporalmente, a buffer de filas. Por supuesto, la solución es siempre suficiente - asegúrese sólo de que no hay cambios pendientes antes de que intente cambiar el modo de buffer.

Controlar datos en buffer en Visual FoxPro

Artículo original: Handling buffered data in Visual FoxPro
http://weblogs.foxite.com/andykramek/2005/12/27/handling-buffered-data-in-visual-foxpro

Autor: Andy Kramek
Traducido por: Ana María Bisbé York


Un par de meses atrás escribí sobre buffer; pero deliberadamente dejé fuera de la discusión dos funciones vitales mientras trabajaba con buffer- nombradas TableUpdate() y TableRevert(). Estas son la base mediante la cual usted, el desarrollador, controla la transferencia de los datos entre el buffer de Visual FoxPro y el origen de datos originales. La función TableUpdate() toma los datos pendientes desde el buffer y los actualiza en la tabla original, mientras TableRevert() refresca los buffers para releer el dato desde el origen de datos. La realización exitosa de otras funciones da como resultado un buffer 'limpio' lo que significa que, para Visual FoxPro, los buffer y el origen de datos son sincronizados.

Controlar el alcance de las actualizaciones

Debido a que Visual FoxPro admite tanto buffer de Filas y de Tablas, ambas funciones de traspaso de datos pueden operar "sólo sobre el registro actual" o sobre "todos los registros modificados". El primer parámetro que se pasa a la función determina el alcance de la operación y ufff tenemos otra confusión posible aquí. Las funciones operan en modo significativamente diferente y tienen tipos diferentes de valores devueltos.

En la versión 3.0 de Visual FoxPro; ambas TableUpdate() y TableRevert() aceptarían solamente un parámetro lógico para determinar el alcance del cambio que están controlando. Pasar un valor de .T., significa que todos los cambios pendientes fueron procesados, mientras .F. restringe las operaciones al registro actual solamente, sin importar el modo de buffer empleado.

TableRevert() devuelve el número de filas que fueron revertidos y no pueden realmente "fallar" - sin que fuera un problema físico, como perdiendo una conexión de red. En una fila de tabla en buffer, o cuando específicamente el alcance como .F., el valor devuelto, por tanto siempre 1.

TableUpdate() devuelve un valor lógico que indica si la modificación especificada es exitosa, independientemente del alcance. En otras palabras, un valor devuelto de .T., indica que todos los registros en el alcance han sido actualizados con éxito. Sin embargo, el comportamiento cuando se utiliza un parámetro lógico para determinar el alcance y la actualización falla por cualquier razón, es que no se genera un error y la función devuelve un valor de .F. dejando el puntero en el registro en que falla.

Si está actualizando un solo registro, esto es muy sencillo; pero si está actualizando múltiples registros en una tabla, y un registro no puede ser actualizado, esto significa que cualquier actualización posterior no puede ser verificada. Pero, después de solucionar el conflicto para el registro que ha fallado, no hay garantía que re-intentar la actualización que no va a fallar al registro más cercano. Esto puede ser un problema!

El comportamiento de TableUpdate() fue, por tanto, modificado en la versión 5 para aceptar incluso un parámetro numérico o lógico para el alcance, donde 0 es equivalente para .F. y 1 para .T. El nuevo comportamiento, que puede ser solamente especificado al pasar "2" como parámetro de alcance, específicamente dirigidos los problemas de actualización de múltiples registros.

Al utilizar buffer de tablas, al llamar TableUpdate() con un parámetro de alcance de "2" Visual FoxPro intenta actualizar todos los registros que tienen cambios pendientes. Sin embargo, si un registro no puede ser actualizado, en lugar de parar, la función registra el número de registro que ha fallado en una matriz (cuyo nombre puede ser especificado como el cuarto parámetro) y continúa tratando de actualizar otros registros cambiados. La función devuelve .F. si cualquier registro falla la actualización; pero al menos tratará de actualizar todos los registros disponibles. La matriz de salida contiene una lista de los números de registros para aquellos registros fallan la actualización.

El segundo parámetro para TableUpdate()

Una diferencia mayor entre la sintaxis de TableUpdate() y TableRevert() es que el anterior puede solamente tomar un parámetro extra lógico, en la segunda posición en la lista de parámetros. Esto controla la forma en la que se comportan las actualizaciones cuando encuentran un conflicto.

De forma predeterminada, Visual FoxPro debe rechazar un cambio cuando un conflicto entre el buffer de datos y se detecta el dato original (vea debajo un debate completo del conflicto y la resolución). Al especificar un valor lógico .T. como el segundo parámetro puede forzar una actualización a que sea aceptada incluso en situaciones cuando debería fallar. Naturalmente esto es algo que deseará hacer de forma predeterminada; pero existen, como veremos luego, situaciones donde este comportamiento no es solamente deseado, sino esencial.

Especificar la tabla a ser actualizada o revertida

Ambas funciones TableUpdate() y TableRevert() operan sobre una tabla al mismo tiempo. El comportamiento predeterminado es que, a menos que específicamente, se determine lo contrario; actuará en la tabla en el área de trabajo seleccionada. Si no hay una tabla abierta en el área de trabajo, hay un error (No se encuentra el alias - Error 13). Ambos, sin embargo, pueden actuar sobre una tabla abierta disponible en la sesión de datos actual y puede aceptar cualquiera de los nombres de ALIAS (el tercer parámetro para TableUpdate(), segundo para TableRevert()) o un número de área de trabajo.

No recomendamos el uso del número del área de trabajo en esto, o cualquiera, situación donde está especificando una tabla diferente que la seleccionada. Tal y como hemos podido ver esta funcionalidad está incluida, sólo por compatibilidad hacia atrás y no tiene lugar en el entorno VFP. Existen dos razones para evitar el uso del área de trabajo. Primeramente, hace su código dependiente de tablas específicas estén abiertas en áreas específicas de trabajo - lo que es aun mayor limitación si piensa cambiar! En segundo lugar, no tiene control real sobre las tablas abiertas en VFP, los cursores o vistas cuando utiliza de cualquier forma el Entorno de datos del formulario. Entonces, liberar el número del área de trabajo en lugar del alias, es una estrategia muy arriesgada e innecesaria.

El único momento que recomendamos el uso del número de área de trabajo es cuando guardamos el área de trabajo actual guardando el valor devuelto de la función SELECT(0). Utilizar el número del área de trabajo en este caso asegura que el área de trabajo actual está vacía, o que las tablas que contienen se cierren durante cualquier operación que esté haciendo, puede aun devolverlo sin error.

Conclusión

Existe mucha funcionalidad y flexibilidad oculta dentro de TableUpdate() y TableRevert(). Al utilizar buffer, necesita tener cuidado de, exactamente cuáles de varias combinaciones de sus parámetros, puede pasarles para asegurar que está utilizando la combinación correcta que necesita. Mientras TableRevert() es bastante simple, TableUpdate() es más compleja y por eso, en la Tabla 2 que muestro debajo se brinda un resumen de algunas combinaciones "prácticas" de parámetros para TableUpdate().

Tabla 2. Opciones de TableUpdate()

Parámetros

AlcanceFuerzaTablaSalidaAcción
0 ó .F..F.  Intenta actualizar la fila actual del alias actual
0 ó .F..T.  Fuerza la actualización del registro actual solamente del alias actual
0 ó .F..F./.T.Alias Intenta forzar todo los registros disponibles de las alias especificadas
1 ó .T..F.  Intenta actualizar la fila actual sólo del alias especificado
1 ó .T..T.   Fuerza la actualización de todos los registros disponibles del alias actual
1 ó .T..F./.T.Alias Intenta actualizar la fila actual sólo del alias especificado. Se detiene en un fallo
2.F.AliasMatrizIntenta actualizar todos los registros disponibles del alias especificado. Nota los fallos; pero no se detiene.
2.T.  Fuerza la actualización de todos los registros disponibles del alias actual/especificado
2.F./.T.Alias MatrizIntenta/Fuerza la actualización de todos los registros disponibles del alias especificado. Nota los fallos; pero no se detiene.

Detectar y solucionar conflictos

La sección anterior trataba sobre los mecanismos para actualizar una tabla con buffer y en mi artículo anterior, recomendé que el Buffer de Tabla optimista debe ser la opción normal para la mayoría de las aplicaciones. Entonces, el próximo problema está, en asegurarse de que los cambios no son la causa de un conflicto de actualización cuando guarda. Uno de los problemas inherentes al utilizar bloqueo optimista en un entorno multiusuario es que es posible para más de un usuario hacer cambios al mismo registro al mismo tiempo. Puede preguntarse ¿porqué este tipo de cosa pudiera ser posible alguna vez?- ¿ está seguro de que dos usuarios no pudieran actualizar el mismo registro al mismo tiempo?

En la práctica, existen muchas situaciones donde puede ocurrir legítimamente. Considere la situación en una Orden de Ventas en el procesamiento del sistema. Cuando se coloca una orden para un elemento, el "stock disponible" actual debe reajustarse para reflejar la reducción. Si dos usuarios, que son controlados por dos operadores simultáneamente, incluyen el mismo elemento en sus órdenes, existe un gran posibilidad de que surja un conflicto. Obviamente, esto puede no ocurrir si el sistema utiliza bloqueo pesimista; pero esto tiene otras consecuencias, generalmente indeseables. En este caso, el segundo operador, que trata de acceder al elemento en cuestión, recibirá un mensaje que el registro está en uso por alguien y no puede hacer modificaciones - no es mucha ayuda ! Aún más, el bloqueo pesimista puede ser utilizado cuando una tabla Visual FoxPro es utilizada directamente como origen de dato - no puede bloquear de forma pesimista una vista de cualquier tipo (ni para datos locales o remotos).

Al utilizar buffer, Visual FoxPro hace una copia de todos los datos como e recuperan de la tabla física, cuando se pide una actualización, se compara esa copia con el estado actual del dato. Si no hay cambios, la actualización es permitida, en otro caso, la actualización se genera un error de conflicto (#1585 para las vistas, #1595 para tablas).

El rol de OldVal() y CurVal()

La base de toda la detección del conflicto está en dos funciones nativas, OldVal() y CurVal(), las que acceden a los cursores intermedios creados por Visual FoxPro cuando utiliza datos en buffer. Como indican sus nombres, OldVal() devuelve el valor de un campo tal y como era cuando el usuario final leyó el dato del origen, mientras CURVAL() devuelve el estado actual del dato en la tabla origen. Estas dos funciones operan desde el nivel de campo y, aunque ambas pueden aceptar una expresión que evalúa una lista de campos, son más usadas para devolver valores de campos individualmente para evitar el problema de solucionar diferentes tipos de datos.

Aquí hay un problema al utilizar CurVal() para verificar el estado de la vista. Visual FoxPro realmente mantiene un nivel adicional de buffer para una vista, la cual, a menos que sea refrescada inmediatamente antes de verificar el valor del campo, puede causar CurVal() que devuelve la respuesta incorrecta.

Entonces, ¿Cómo puedo realmente detectar el conflicto?

Antes de entrar en la discusión de cómo detectar un conflicto, permítanme ser claro sobre la definición de Visual FoxPro sobre lo que es un conflicto. Como he comentado antes, en la discusión sobre buffering, Visual FoxPro realmente hace dos copias de un registro, cada vez que el usuario accede al dato. Una copia está disponible como cursor modificable y es donde un usuario hace los cambios. La otra copia guarda el estado original. Antes de permitir la actualización, Visual FoxPro compara este cursor original con el dato guardado actualmente en el disco. Ocurre un conflicto cuando estas dos versiones del dato no coincide exactamente, y existen dos formas en las que puede ocurrir. La primera, y más obvia, es debido a que el usuario actual hace cambios a un registro y trata de guardar esos cambios después de que otro ha cambiado y guardado el mismo registro.

La segunda es menos obvia y ocurre cuando un usuario hace un cambio; pero entonces los cambios van a sus valores originales. El resultado es que no cambia cuando en realidad hace; pero cuando tratan de "guardar" el registro Visual FoxPro va a ver aun esto como un conflicto debido a que los valores OldVal() y CurVal(), en realidad son diferentes. Para evitar este tipo de error puede simplemente confiar en GetFldState(); pero debe comparar expresamente los valores en el buffer actual con estos en OldVal().

Entonces, habiendo evitado la posibilidad de conflictos cuando el usuario actual no ha hecho ningún cambio; pero sencillamente no trata de confirmar el registro, existen básicamente dos estrategias que puede adoptar para detectar conflictos. La primera es el proceder "Trate y vea". Esto significa simplemente que no trata y detecta conflictos potenciales; pero sólo atrapa el resultado de una llamada para TableUpdate() y cuando falla, determina porqué.

El segundo proceder es "Cinturones y tirantes" en el que cada campo cambiado se verifica individualmente y cualquier conflicto se soluciona antes de intentar actualizar la tabla original. Mientras esto parece más defensivo, y por tanto "mejor" en realidad oculta un problema. En el tiempo que esto ocurre (aunque sea muy pequeño) para verificar todos los cambios contra sus valores actuales, otro usuario puede cambiar exitosamente el mismo registro que está verificando. Entonces, a menos que también haya bloqueado explícitamente el registro antes de comenzar a verificar los valores, la actualización real pudiera fallar. Debido a que queremos realmente evitar explícitamente la colocación de bloqueos, necesita incorporar exactamente la misma verificación del resultado de TableUpdate(), y brinda el mismo control del fallo, lo que es mucho más simple precisamente la estrategia "Intenta y veremos".

Por tanto, a menos que tenga una razón para sobreescribir cambios pre-validados, recomendamos fuertemente que permita que Visual FoxPro detecte los conflictos y justamente atrápelo para aquellos errores que han surgido.

De acuerdo, entonces, habiendo detectado un conflicto de actualización, ¿qué puedo hacer sobre esto?

He aquí cuatro estrategias básicas para actualizar conflictos. Puede escoger una, o combine más de una en una aplicación en dependencia de la situación real:

[1] El usuario actual siempre gana - Esta estrategia es apropiada solamente en aquellas situaciones en las que se asume que son correctos los datos del usuario que está actualmente intentando guardar. Típicamente esto debería ser implementado en la base del ID del usuario, el que es en realidad está haciendo el guardado y debería implementar una regla de negocio que cierta información de una gente es más útil que otra.

Un ejemplo podría ser tener una aplicación donde un operador hable al usuario podría tener derechos de sobreescritura para contactar información para el cliente (en la base que la persona realmente habla al cliente es más probablemente capaz de obtener los detalles correctos). El conflicto puede surgir en este caso cuando un administrador está actualizando un detalle de cliente desde un dato en archivo o última orden, mientras un operador tiene detalles nuevos, directamente desde el cliente. La implementación de esta estrategia en Visual FoxPro es muy sencilla. Simplemente establezca el parámetro "FORCE", (el segundo) en la función TableUpdate() a ".T." y reintente la actualización.

[2] El usuario actual siempre pierde - Esto es exactamente lo contrario a la anterior. Al usuario actual se le permite solamente guardar los cambios siempre que no hay otro usuario que haya hecho cambios. Por el contrario, será implementado normalmente en base al ID del usuario y podría reflejar la probabilidad que este usuario en particular es propenso a trabajar con información "histórica" en lugar de información "actual". La implementación en Visual FoxPro es también muy sencilla. Los cambios del usuario actual se revierten, se recarga la información original y el usuario tiene que hacer cualquier cambio que necesite, una vez más. Esta es, probablemente la estrategia que es adoptada más frecuentemente - pero usualmente en base global.

[3] El usuario actual gana a veces - Esta estrategia es la más compleja de las cuatro a implementar; pero es en la actualidad, bastante frecuente. El principio básico es que cuando ocurre un conflicto de actualización, usted determina si alguno de los campos, que el usuario actual ha cambiado, van a afectar los cambios hechos por otro usuario. Si no, el registro del usuario actual es actualizado automáticamente (utilizando el valor de CURVAL()), entonces esto provoca que es negado el conflicto y la actualización se reintenta. Sin embargo, debido a no puede cambiar los valores devueltos por OldVal(), necesita forzar la segunda actualización.

Incidentalmente, esta estrategia también está dirigida al problema de cómo controlar el conflicto de actualización "falso positivo". Esto ocurre cuando existen discrepancias entre los valores del disco y aquellos en el buffer de usuario, pero los actuales cambios del usuario, no crean en realidad un conflicto con cualquier otro cambio que se haya hecho. Claramente, esta situación no es en realidad un conflicto; pero necesita ser controlada.

Aunque no es trivial, la implementación en Visual FoxPro es relativamente fácil. Primero, la función CURVAL() es utilizada para determinar cómo actualizar el buffer de usuario actual de tal forma que no va sobreescribir los cambios hechos por otro usuario. Entonces, la actualización se aplica utilizando el segundo parámetro FORCE en el TableUpdate() para decirle a Visual FoxPro que ignore el conflicto que van a surgir porque OldVal() y CurVal() no corresponden.

[4] El usuario actual decide - Este es el caso de "Coge todo". El conflicto no falla bajo ninguna regla de negocio reconocida, así que la única solución es preguntarle al usuario cuya acción de guardar desencadena el conflicto que es lo que desea hacer. La idea básica es que usted muestre al usuario que ha desencadenado el conflicto con una lista de valores - aquellos que acaba de entrar) y el valor que hay ahora en la tabla ( es decir con el cual alguien ha cambiado el valor original). El usuario puede entonces decidir si hay que forzar o revertir sus propios cambios. Las herramientas básicas para la implementación de esta estrategia han sido discutidos en secciones anteriores. Todo lo requerido es determinar qué cambios traen conflicto y los presentan al usuario como vía para que el usuario pueda decidir en un campo a campo qué hacer, en base al conflicto. En la práctica, esta estrategia en general se combina con la estrategia [3] mostrada antes, así al usuario sólo se le presenta una lista de campos donde hay un conflicto en los campos que han modificado por ellos mismos.

En el próximo artículo de esta serie, veré el diseño e implementación de una clase que controle un conflicto que puede ser arrastrada a un formulario.

7 de noviembre de 2015

Creando un servidor COM de subproceso múltiple (Parte 3 de 3)

Tercera y última parte del artículo "Creando un servidor COM de subproceso múltiple" escrito por Antonio Muñoz de Burgos y Caravaca (eMans).


Parte 3 de 3

ANEXOS


READ,@...Get/Says
Comandos y funciones de menús, emergentes y barras.
MESSAGEBOX() y WAIT WINDOW


GENERAN ERRORES EN TIEMPO DE EJECUCIÓN

;@…BOX ;@…CLASS ;@…CLEAR
;@…EDIT ;@…FILL ;@…GET
;@…MENU ;@…PROMPT ;@…SAY
;@…SCROLL ;@…TO _ALIGNMENT
_ASSIST _BEAUTIFY _BOX
_CALCMEM _CALCVALUE _CONVERTER
_COVERAGE _CUROBJ _DBLCLICK
_DIARYDATE _FOXDOC _GALLERY
_GENMENU _GENPD _GENSCRN
_GETEXPR _INDENT _LMARGIN
_PADVANCE _PBPAGE _PCOLNO
_PCOPIES _PDRIVER _PDSETUP
_PECODE _PEJECT _PEPAGE
_PLENGTH _PLINENO _PLOFFSET
_PPITCH _PQUALITY _PSCODE
_PSPACING _PWAIT _RMARGIN
_RUNACTIVEDOC _SCCTEXT _SPELLCHK
_STARTUP _TABS _THROTTLE
_TRANSPORT _WRAP ACCEPT
ACTIVATE MENU ACTIVATE POPUP ACTIVATE SCREEN
ACTIVATE WINDOW AGETCLASS() AMOUSEOBJ()
ANSITOOEM() ASELOBJ() ASSERT
ASSIST BAR() BARCOUNT()
BARPROMPT() BROWSE CALL
CHANGE CLEAR DEBUG CLEAR GETS
CLEAR MACROS CLEAR MENUS CLEAR POPUPS
CLEAR PROMPT CLEAR READ CLOSE DEBUGGER
CLOSE FORMAT CLOSE MEMO CNTBAR()
CNTPAD() COL() CREATE
CREATE CLASS CREATE CLASSLIB CREATE COLOR SET
CREATE FORM CREATE LABEL CREATE MENU
CREATE PROJECT CREATE QUERY CREATE REPORT
CREATE SCREEN DEACTIVATE MENU DEACTIVATE POPUP
DEACTIVATE WINDOW DEBUG DEBUGOUT
DEFINE BAR DEFINE BOX DEFINE MENU
DEFINE PAD DEFINE POPUP DEFINE WINDOW
EDIT   FKLABEL()
FKMAX() GETBAR() GETCOLOR()
GETCP() GETDIR() GETEXPR()
GETFILE() GETFONT() GETPAD()
GETPICT() GETPRINTER() HELP
HIDE MENU HIDE POPUP HIDE WINDOW
IMESTATUS() INPUT KEYBOARD
LOAD LOCFILE() MCOL()
MDOWN() MENU MENU TO
MENU() MESSAGEBOX() Comandos MODIFY
MOUSE MOVE POPUP MOVE WINDOW
MRKBAR() MRKPAD() MROW()
MWINDOW() OBJNUM() OBJVAR()
OEMTOANSI() ON BAR() ON ESCAPE
ON EXIT Commands ON KEY ON KEY LABEL
ON PAD ON PAGE ON READERROR
ON SELECTION BAR ON SELECTION MENU ON SELECTION PAD
ON SELECTION POPUP PAD() PLAY MACRO
POP KEY POP MENU POP POPUP
POPUP() PRMBAR() PRMPAD()
PROMPT() PUSH KEY PUSH MENU
PUSH POPUP PUTFILE() RDLEVEL()
READ READ MENU READKEY()
REGIONAL RELEASE BAR RELEASE MENUS
RELEASE PAD RELEASE POPUPS RELEASE WINDOWS
RESTORE MACROS RESTORE SCREEN RESTORE WINDOW
ROW() SAVE MACROS SAVE SCREEN
SAVE WINDOWS SCROLL SHOW GET(S)
SHOW MENU SHOW OBJECT SHOW POPUP
SHOW WINDOW SIZE POPUP SIZE WINDOW
SKPBAR() SKPPAD() SUSPENDER
VARREAD() WAIT WBORDER()
WCHILD() WCOLS() WEXIST()
WFONT() WLAST() WLCOL()
WLROW() WMAXIMUM() WONTOP()
WOUTPUT() WPARENT() WREAD()
WROWS() WTITLE() WVISIBLE()
XMINIMUM() ZOOM WINDOW  


NO GENERAN ERRORES EN TIEMPO DE EJECUCIÓN

 

DOEVENTS    
SET ASSERTS SET BELL SET BORDER
SET BROWSEME SET BRSTATUS SET CONSOLE
SET COLOR SET CLEAR SET CLOCK
SET COVERAGE SET CONFIRM SET CURSOR
SET CPDIALOG SET DEBUGOUT SET DEBUG
SET DEVELOPMENT SET DELIMITERS SET DISPLAY
SET DOHISTORY SET ESCAPE SET ECHO
SET EVENTLIST SET EVENTTRACKING SET FORMAT
SET FUNCTION SET HELP SET INTENSITY
SET MARK OF SET MACDESKTOP SET MACKEY
SET MARGIN SET MESSAGE SET NOTIFY
SET ODOMETER SET PALETTE SET PDSETUP
SET READBORDER SET REFRESH SET RESOURCE
SET SAFETY SET SKIP OF SET STICKY
SET STATUS SET SYSMENU SET TALK
SET TRBETWEEN SET TYPEAHEAD SET VIEW
SET WINDOW SYS(1037) SYS(18)
SYS(103) SYS(2002) SYS(1270)
SYS(2017) SYS(4204) SYS(2016)

 

Entradas en el Registro del Sistema
(enlace del documento: Creando un Servidor COM)

Cuando registramos nuestro componente, se crean varias claves en el registro del Sistema, con las imágenes expuestas podemos ver como quedan en el registro del Sistema.

Podemos ver la Biblioteca en tiempo de ejecución que será usada en la llave: Foxruntime, la ruta donde se encuentra registrado nuestro componente, etc.

Estas entradas se registran en HKEY_CLASSES_ROOTCLSID{aquí GUID generado al compilar}

Si realizas compilaciones donde vuelvas a generar el GUID, es conveniente que previamente quites las entradas del registro, de forma contraria te encontraras que tienes el registro del Sistema con un montón de basura.
(regsvr32 /u MiComponente.dll)


Este el fichero miComponenteWeb.VBR que se ha generado al compilar el componente del ejemplo.

VERSION=1.0.0

HKEY_CLASSES_ROOTmicomponenteweb.MiComponenteWeb = micomponenteweb.MiComponenteWeb
HKEY_CLASSES_ROOTmicomponenteweb.MiComponenteWebNotInsertable
HKEY_CLASSES_ROOTmicomponenteweb.MiComponenteWebCLSID = {C735E044-08DD-4F98-A0A3-6561B4EC10DD}
HKEY_CLASSES_ROOTCLSID{C735E044-08DD-4F98-A0A3-6561B4EC10DD} = micomponenteweb.MiComponenteWeb
HKEY_CLASSES_ROOTCLSID{C735E044-08DD-4F98-A0A3-6561B4EC10DD}ProgId = micomponenteweb.MiComponenteWeb
HKEY_CLASSES_ROOTCLSID{C735E044-08DD-4F98-A0A3-6561B4EC10DD}VersionIndependentProgId = micomponenteweb.MiComponenteWeb
HKEY_CLASSES_ROOTCLSID{C735E044-08DD-4F98-A0A3-6561B4EC10DD}InProcServer32 = micomponenteweb.dll
HKEY_CLASSES_ROOTCLSID{C735E044-08DD-4F98-A0A3-6561B4EC10DD}InProcServer32"ThreadingModel" = Apartment
HKEY_CLASSES_ROOTCLSID{C735E044-08DD-4F98-A0A3-6561B4EC10DD}TypeLib = {3371A7EA-3BDA-4101-84AC-51A32B9F9E3B}
HKEY_CLASSES_ROOTCLSID{C735E044-08DD-4F98-A0A3-6561B4EC10DD}Version = 1.0
HKEY_CLASSES_ROOTCLSID{C735E044-08DD-4F98-A0A3-6561B4EC10DD}Foxruntime = VFP7T.DLL
HKEY_CLASSES_ROOTINTERFACE{5E398B91-C7E1-41DD-B66F-DE22CED073C9} = MiComponenteWeb
HKEY_CLASSES_ROOTINTERFACE{5E398B91-C7E1-41DD-B66F-DE22CED073C9}ProxyStubClsid = {00020424-0000-0000-C000-000000000046}
HKEY_CLASSES_ROOTINTERFACE{5E398B91-C7E1-41DD-B66F-DE22CED073C9}ProxyStubClsid32 = {00020424-0000-0000-C000-000000000046}
HKEY_CLASSES_ROOTINTERFACE{5E398B91-C7E1-41DD-B66F-DE22CED073C9}TypeLib = {3371A7EA-3BDA-4101-84AC-51A32B9F9E3B}
HKEY_CLASSES_ROOTINTERFACE{5E398B91-C7E1-41DD-B66F-DE22CED073C9}TypeLib"Version" = 1.0


; TypeLibrary registration
HKEY_CLASSES_ROOTTypeLib{3371A7EA-3BDA-4101-84AC-51A32B9F9E3B}
HKEY_CLASSES_ROOTTypeLib{3371A7EA-3BDA-4101-84AC-51A32B9F9E3B}1.0 = micomponenteweb Type Library
HKEY_CLASSES_ROOTTypeLib{3371A7EA-3BDA-4101-84AC-51A32B9F9E3B}1.0win32 = micomponenteweb.dll
HKEY_CLASSES_ROOTTypeLib{3371A7EA-3BDA-4101-84AC-51A32B9F9E3B}1.0FLAGS = 0
 

IMÁGENES DEL REGISTRO



Bibliografía y/o documentación adicional:

Biblioteca MSDN & Resource KIT.
Manual de Referencia de Visual FoxPro.


Antonio Muñoz de Burgos y Caravaca
www.emans.com (Web realizada en vFoxPro)
Sevilla - España
Manifestando el apoyo a la comunidad de desarrolladores de MS Visual FoxPro.
 

Todas las marcas aquí mencionadas, están registradas por sus respectivos fabricantes.