17 de septiembre de 2002

Búsquedas sencillas

Autor: Les Pinter

De cúantas maneras quieren tus usuarios hacer consultas? Probablemente demasiadas.

No te cuento cuántas veces mis usuarios me han dicho "quiero poder buscar lo que sea." Pero cuando les digo que esa posibilidad no viene incluido en el lenguaje, sino que va a costar dinero, y que dependiendo de como lo haga puede ser increiblemente lento, usualmente piden otra cosa - algo rápido y barato.

Afortunadamente hay por lo menos dos maneras de añadir un grupo de consultas a cualquier pantalla. Se hace en unos cuantos minutos, y dado el precio, la mayoría de los usuarios descubrirán que hace todo lo que quieren - o al menos, todo lo que están dispuestos a pagar. Lo cual es precisamente lo que queríamos saber en primer término...

1 - Usa un marco de página con una Listbox cuyas columnas pueden ser ordenadas en la última página.

Este truco se aprovecha del hecho de que en el caso de las ListBox con RowSourceType = 2 (Alias), el puntero de la tabla se mueve a medida que el usuario cambia el registro seleccionado en la ListBox. (RowSourceType = 2 quiere decir usa los primeros n campos de la tabla como fuente de datos para las columnas del control, mientras que RowSourceType = 6 (Campos) significa que vas a especificar los nombres y el orden de los campos que aparecen en las columnas del control. Al pasar el usuario por los registros mostrados en el control, el puntero de la tabla o el cursor se mueve correspondientemente. Así que el puntero de la tabla siempre está de acuerdo con el muestreo. Pero, me preguntas, esto ¿cómo me ayuda con las búsquedas?

Consideremos qué debe lograr una consulta. Se usa una consulta para localizar algo rápidamente. Tradicionalmente, usando SQL, hacemos una consulta que le muestra al usuario todo lo que se asemeja a lo que busca, y le dejamos escoger el registro exacto de esa selección reducida. Pero esto implica una TextBox para permitir que escriban lo que quieren buscar, lo cual puede contener errores ortográficos; y ¿buscamos mayúsculas o minúsculas? ¿Hay cupo en la pantalla para entrar la clave? ¿Donde pondremos la lista de candidatos a seleccionar? Y ¿qué haremos con los registros que no hacen juego con lo que entró el usuario pero sí son lo que está buscando?

En vez de hacer todo esto, aprovechemos el hecho de que una tabla FoxPro con todos sus registros ya está disponible. Sencillamente vamos a mostrar una grid con las columnas más importantes que le permitirá al usuario seleccionar visualmente, cambiando el orden de una u otra columna hasta ver lo que estaba buscando. Y lo mejor es que se puede hacer esto con sólo unas cuantas líneas de código.

Como siempre, FoxPro proporciona un mecanismo muy sencillo para hacer esto. Primero, añadimos un método llamado ColumnSort al formulario. Si usas una clase base para todos tus formularios, práctica que recomiendo sin reservaciones, añade esta clase a la clase de formulario que usas como plantilla. La sentencia PARAMETER debe ser la primer línea de código en el método:
* ColumnSort method
PARAMETER IndexTagName
WITH THISFORM
SELECT (.DataEnvironment.InitialSelectedAlias)
SET ORDER TO ( IndexTagName )
.LockScreen   = .T.
.PageFrame1.Pages[.PageCount].MyList1.Refresh
.LockScreen   = .F.
ENDWITH
Yo siempre escribo el nombre de la tabla o alias principal de cada formulario en la propiedad InitialSelectedAlias del Entorno de Datos del mismo. Esto me permite escribir código genérico para habilitar métodos como BorrarRegistro, ProximoRegistro, etc.

Luego, como lo implica el códogo, arriba, añade un marco de página (PageFrame) al formulario y pon una ListBox en la última página. Como RowSourceType usa 6, y como RowSource el nombre de la tabla principal, más los nombres de las columnas que aparecen en la tabla, e.g. "CLIENTES.Nombre,Telefono,Ciudad,Estado,Contact", usando los nombres que aparecen en los encabezados de las columnas de la ListBox. Se usa la propiedad ColumnCount de la ListBox para precisar el número de columnas, y el número de píxeles de anchura de cada columna, separadas con comas en la propiedad ColumnWidth. Se suele usar Max(anchura del valor más ancho que aparecerá en la columna,anchura del título que aparece en el encabezado) como la anchura para cada columna.



Después de poner una etiqueta encima de cada columna, usa lo que sigue en el evento Click de cada una de las etiquetas:
THISFORM.SetIndex ( THIS.Caption )
El método SetIndex cambia el orden de la tabla y refresca el contenido que aparece en la lista. (Si tu índice no tiene el mismo nombre que la etiqueta, sigue leyendo - no es difícil manejar tales casos.)
PROCEDURE SetIndex
PARAMETERS   IdxName
WITH THISFORM
SELECT (.DataEnvironment.InitialSelectedAlias )
SET ORDER TO ( IdxName )
.PageFrame1.Page2.MyList1.Requery
ENDWITH
Finalmente, esto es el código DblClick de la ListBox:
PROCEDURE mylist1.DblClick
WITH THISFORM
.LockScreen   = .T.
.PageFrame1.ActivePage   = 1
.PageFrame1.Page1.Refresh
.LockScreen   = .F.
ENDWITH
Esto activa la primera página, la cual muestra el contenido de los demás campos del registro seleccionado mediante el puntero de la ListBox.

Como indicamos, esto asume que se usan TAGS de índice cuyos nombres son exactamente los mismos que las capciones de las etiquetas que sirven de encabezados para la ListBox en la última página. Si prefieres usar nombres de índices que no se pueden usar como TAGS de índice (por ejemplo, capciones que contienen espacios), el método SetIndex debe incluir una forma de usar la capción para encontrar el nombre del TAG, a saber:
SET ORDER TO TAG ( ;
IIF(IdxName=[Nombre del Cliente],[Nombre], ;
IIF(IdxName=[Apellido del Cliente],[Apellido] , IdxName)))

Un Doble-Click en la lista nos lleva a la primera página:



Qué pasa si hay muchísimos registros?

Desafortunadamente, este método es demasiado lento cuando se usa con tablas grandes en una máquina con memoria limitada. En una P266 con 32MB de memoria, trabaja bien hasta llegar a los 10,000 registros. Pero si a FoxPro le falta memoria para guardar el índice actual en la memoria, se vuelve lenta la búsqueda - cosa que los usuarios acostumbrados a la rapidez de FoxPro no toleran muy bien. Por esto, para efectuar búsquedas de tablas con muchos registros, es mejor crear una pantalla de búsqueda separada.

2 - Usa una pantalla de consulta para retornar la clave deseada

Para lograr esto, añade al forulario un botón de comando que a su vez abre otro formulario que contiene una ListBox para mostrar los resultados de la consulta. Pon una combobox y una textbox al fondo de la pantalla, para permitirle al usuario precisar las características que quiere usar para localicar registros. El combobox especifica el nombre del campo a usar, y el textbox el valor a buscar en el campo especificado.

El evento Valid del textbox es entonces responsable por cargar todos los registros que contienen el valor especificado en el campo seleccionado. El usuario usa una DobleClick en uno de los registros que aparecen en la lista para seleccionar un valor y retornar su código. El código del botón de consulta del formulario que abrió la pantalla de consulta entonces usa el valor retornado para encontrar y mostrar el registro seleccionado.



Dentro del formulario de consulta, el código Valid del combobox asegura el formateo correcto de la clave entrada por el usuario:
THISFORM.ActiveTag = THIS.Value
WITH THISFORM.MyText1
DO CASE
  CASE THIS.Value = "Name"
     .Format = "K"
     .InputMask = "!!!!!!!!!!"
     THISFORM.KeyField = [UPPER(LastName)]
     THISFORM.OrderField = [LastName]
  CASE THIS.Value = "Company"
     .Format = "K"
     .InputMask = "!!!!!!!!!!"
     THISFORM.KeyField = [UPPER(Company)]
     THISFORM.OrderField = [Company]
  CASE THIS.Value = "Zip"
     .Format = "K"
     .InputMask = "#####"
     THISFORM.KeyField = [Zip]
     THISFORM.OrderField = [Zip]
  CASE THIS.Value = "Phone"
     .Format = "K"
     .InputMask = "(###)########"
     THISFORM.KeyField = [Phone]
     THISFORM.OrderField = [Phone]
  CASE THIS.Value = "CustNo"
     .Format = "K"
     .InputMask = "#####"
     THISFORM.KeyField = [CustNo]
     THISFORM.OrderField = [CustNo]
ENDCASE
.Visible  = .T.
ENDWITH
THISFORM.Input = SPACE(30)
KEYBOARD [{RightArrow}]
El código del método Valid de la TextBox carga y muestra todos los registros que cumplen con los requisitos del usuario:
WITH THISFORM.MyList1
.RowSourceType  = 0
.Clear
ENDWITH

WITH THIS
* Crea la clave para el número de teléfono,
*  quitándole todo lo que no sea un dígito:
IF .Value  = [(]
   IF LEN(TRIM(.Value)) > 5
    .Value = LEFT(THIS.Value,8) + [-] ;
     + IIF(SUBS(THIS.Value,9,1)=[-], ;
           SUBS(THIS.Value,9), ;
           [-]+SUBS(THIS.Value,9))
   ENDIF
ENDIF
Key = TRIM(.Value)
SELECT CUSTOMER
SET ORDER TO TAG ( THISFORM.Mycombo1.Value )
SET EXACT OFF   && Importante! Permite claves parciales
* will not match if SET("EXACT") = "ON"
SEEK Key
DO WHILE ( NOT FOUND() ) AND ( LEN(Key) > 0 )
   Key = LEFT ( Key, LEN(Key)-1 )
   SEEK Key
ENDDO
ENDWITH

IF ( NOT FOUND() ) OR ( LEN(Key) = 0 )
   IF ( NOT FOUND() )
      MessageBox ( "No match", 64, "My App" )
   ENDIF
   THISFORM.Init
   RETURN
ENDIF
WITH THISFORM
.LockScreen   = .T.
.MousePointer = 11
ENDWITH
WITH THISFORM.MyList1
.RowSourceType = 0
cmd = [SELECT lastname,firstname,company,phone,zip,custno] ;
    + [ FROM CUSTOMER] ;
    + [ WHERE ]+THISFORM.KeyField+[ = "]+Key+["] ;
    + [ INTO CURSOR Matches] ;
    + [ ORDER BY ] + THISFORM.OrderField
&cmd
.RowSource     = "Matches"
.RowSourceType = 2
.Requery
.Refresh
.Selected[1]   = .T.
ENDWITH
WITH THISFORM
.SetAll  ( "ForeColor", RGB(0,0,0),"MyLabel")
* SORT NO LONGER APPLIES
.LockScreen    = .F.
.MousePointer  = 1
ENDWITH
Yo le cambio el color de los encabezados de las columnas para indicar visualmente que los datos ya están ordenados:

He aquí el código del evento DblClick de la ListBox:
THISFORM.Release
Y el código del método UNLOAD del formulario es esto:
RETURN THISFORM.MyList1.Value
En el código Click del botón de comando de Búsqueda del formulario original hay que tener esto:
DO FORM Search TO m.CustCode
IF NOT EMPTY ( m.CustCode )
   SEEK m.CustCode
   THISFORM.Refresh
ENDIF
Existen decenas de variaciones que pueden ser derivadas de este método básico. En mi ejemplo, deshabilité el mecanismo de consulta una vez que hayan seleccionado un campo de búsqueda de la ComboBox. Si quieren efectuar otra consulta, tienen que cerrar el formulario y volverlo a seleccionar, del mismo formulario, de otro, o del menú. En algunas aplicaciones sería preferible permitirle al usuario quedarse en la misma pantalla y hacer cuantas consultas quiere antes de seleccionar un registro y retornar - por ejemplo, para buscar padrones en una tabla de facturas antiguas para una auditoría.

Se pueden construir consultas sofisticadas para otros casos. Por ejemplo, para seleccionar todos los registros padres que tiene al menos un registro hijo con determinada característica - todas las facturas en las cuales una de las cosas compradas fué un podador de césped, por ejemplo - se puede crea una subconsulta:
SELECT &FldList. FROM INVOICES ;
 WHERE INVNO IN                     ;
 ( SELECT INVNO FORM INDETAIL  ;
    WHERE [LAWNMOWER] $ UPPER(DESC) ) ;
    ORDER BY INVDATE DESC INTO CURSOR Matches
También se pueden crear subconsultas que incluyen una lista determinada de valores - por ejemplo, las facturas de los tres clientes más importantes:
SELECT &FldList. FROM INVOICES   ;
WHERE CustNo IN ("00102","00352","00601")

No hay comentarios. :

Publicar un comentario

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