Llevaba varias semanas con la idea de hacer un par de comentarios breves sobre el componente TDBLookupComboBox y mira por donde, hoy parece que tengo unos minutos que voy a intentar aprovechar.

Así que vamos a ello. La idea es ayudar con algunas ideas a los compañeros que dan sus primeros pasos.

Este componente es bastante clásico (no es nuevo). Y está ubicado en la paleta “DataControl”, la paleta que Borland reservó en sus primeros días para alojar todos los controles de datos y se justifica, en un contexto donde el usuario selecciona un item entre los valores mostrados en una lista ligada a un conjunto de datos, para asignar un valor (como resultado de esta selección), en un campo del dataset destino. Así pues, este valor que va a ser asignado, es por regla general distinto del valor que es mostrado al usuario y también siguiendo el mismo razonamiento, formaría parte de un dato significativo de una tabla maestra respecto a su detalle (eso es algo lógico ¿no?). Digo “por regla general” ya que realmente no existe ninguna restricción que impida que los campos Keyfield y ListField se asignen al mismo nombre de campo pero esto no tendría demasiado sentido. Sería similar a la funcionalidad ofrecida por el componente TDbComboBox, salvando el hecho de que este obtiene su lista de selección sobre una lista de cadenas, cargada en memoria mediante algunos de los métodos que publica la clase TStrings.

Por poner un ejemplo que nos sirva de referencia en esta entrada, podemos imaginar una tabla de avisos muy sencilla, que nos servirá como recordatorio de una tarea, y en la que podemos anotar la fecha-hora,  el cliente, el comentario o la observación a recordar y si dicha tarea está activa o no lo está. La estructura habitual de un tabla así, podría ser similar a la generada por este script de sql:

CREATE TABLE [dbo].[Avisos] (
  [IDAviso] int IDENTITY(1, 1) NOT NULL,
  [IDCliente] int NOT NULL,
  [Fecha] datetime NOT NULL,
  [Comentario] varchar(80) NULL,
  [Activo] char(1) NULL,
  PRIMARY KEY CLUSTERED ([IDAviso])
)
ON [PRIMARY]
GO

Y siguiendo el ejemplo, se supone que debería existir otra tabla ligada a los clientes, que vamos a limitar a tres campos que puedan ser significativos, en este contexto (el identificador del cliente, el código del cliente y finalmente su nombre).

CREATE TABLE [dbo].[CLIENTES] (
  [IDCliente] int IDENTITY(1, 1) NOT NULL,
  [Codigo] int NOT NULL,
  [Nombre] varchar(40) NOT NULL,
  PRIMARY KEY CLUSTERED ([IDCliente])
)
ON [PRIMARY]
GO

Podría además existir definida en la base de datos la relación de clave ajena que permite vincular los valores almacenados en el campo IDCliente de la tabla avisos con los valores existentes en la tabla de clientes, pero a efectos de estos comentarios no es algo relevante, por lo que yo creo que podemos obviarlo y simplemente suponer que existe.

Para acompañar la entrada, he querido también preparar un pequeño ejemplo que recree lo que estamos compartiendo pero, como no he querido usar una conexión a base de datos, para no complicar el escenario con algo que tampoco es imprescindible, he añadido en el módulo de código un procedimiento para recrear la creación de nuestros datasets, que se ejecuta durante la creación del formulario.

procedure TMain.InicializacionDatosEjemplo;

Ya tenemos casi todos los ingredientes.

Si ojeais el procedimiento para inicializar los datos y la estructura del dataset, vereis que la tabla de Avisos contiene tanto el IDCliente como los campos Codigo y Nombre, aunque no pertenezcan a dicha tabla sino a la de Clientes.  Os incluyo una imagen que os muestra los campos que contienen cada ClientDataSet (en el codigo de inicialización he incluido para que sea mas didáctico, unas rutinas para crear los campos persistentes que están comentadas, ya que la creación la hice en tiempo de diseño)

fields

Convenimos que podría ser algo razonable, fruto de una consulta sql similar a:

SELECT
  dbo.Avisos.IDAviso,
  dbo.Avisos.IDCliente,
  dbo.CLIENTES.Codigo as CodigoCliente,
  dbo.CLIENTES.Nombre as NombreCliente,
  dbo.Avisos.Fecha,
  dbo.Avisos.Comentario,
  dbo.Avisos.Activo
FROM
dbo.Avisos INNER JOIN dbo.Clientes ON
(dbo.CLIENTES.IDCliente = dbo.Avisos.IDCliente)

Esto no es nada extraño. Sería un efecto similar al haber definido los campos CodigoCliente y NombreCliente como Lookup vinculados a una consulta abierta al dataset del cliente, en lugar de haber sido creados como tipo Data, que es el caso referido. No… no digo que esté mal o bien  🙂  y si estas dando tus primeros pasos quizás lo veas hasta lógico que sea indicado. A mi también me pasaba… Pero la picaresca es muy útil y nos basta una pequeña “trampa” legal para evitar que los susodichos campos sean actualizables, obra y gracia de nuestro TClientDataSet. ¿Hay alguna razón de que todos los campos sean actualizables, contrastables y participen en la sentencia retornada a la base de datos…?

Ya sabeis por donde voy ¿no? Nos basta una simple asignacion a false en los valores pfInUpdate y pfInWhere de las opciones providerflags de cada uno de los dos campos. Y quedan los dos a nuestra merced, para que puedan ser alterados durante el proceso de grabación y confirmación de cambios (sin que afecten a nada). Eso sí… la sentencia sql debería referir en la parte que vincula una tabla con otra tras el FROM, siempre en primer lugar la tabla que va a ser actualizada, que en este caso concreto es la tabla de Avisos.

Seguimos avanzado en el ejemplo y diseñamos un formulario, que va a mostrar una rejilla en la parte superior y lo que pudiera ser una ficha editable en la parte inferior. El poner la rejilla es para hacerlo un poco más gráfico y visual y que se aprecie como dos cosas distintas, puesto que en todos los frameworks que hemos compartidos han existido estos dos elementos.

Esta es la imagen de nuestro único formulario:

formularioDis

Descargar código fuente

Ahora nos resta escribir unas lineas de código, en el Evento OnValidate del campo CodigoCliente:

procedure TMain.cdsAvisosCodigoClienteValidate(Sender: TField);
begin
  if not Sender.IsNull then
  begin
     if cdsClientes.Locate('Codigo', Sender.Value, []) then
     begin
       cdsAvisos.FieldByName('IDCliente').Value:= cdsClientes.FieldByName('IDCliente').Value;
       cdsAvisos.FieldByName('NombreCliente').Value:= cdsClientes.FieldByName('Nombre').Value;
     end
     else raise Exception.Create('No se ha encontrado el cliente');
  end
  else begin
     cdsAvisos.FieldByName('IDCliente').Clear;
     cdsAvisos.FieldByName('NombreCliente').Clear;
  end;
end;

El efecto que estamos logrando es el de coordinar de una forma sencilla, tanto que pueda haber sido usado el selector de clientes como la casilla en la que el usuario va a introducir el código, dato que en un porcentaje alto de casos es sabido de antemano por el usuario.

Eso es algo bastante normal y corriente, que existan junto con la clave primaria, otras claves naturales que conviven y se garantizan mediante indices únicos. Imaginemos una tabla de pedidos (por poner un ejemplo claro), con una clave primaria que podría ser de tipo entero, incremental. Y una clave natural formada por el Ejercicio, la Empresa y el numero de pedido (y quizás la serie también si la empresa hiciera uso de series). La clave primaria en esos casos nunca es del dominio del usuario sino que es usada internamente por la aplicación para apuntar a los registros, mientras que la clave natural, en cambió, si es usada por el usuario al interactuar con el interfaz de la aplicación.

Pensareis: si hubieramos apuntado el campo KeyField de nuestro TDBLookupComboBox a IDCliente tambien hubiera funcionado sin ese código añadido en el evento OnValidate ¿no?.  Pues sí, pero fácilmente crearíais referencias circulares entre ambos, al intentar coexistir y modificarse uno a otro tras la acción del usuario. Al apuntar los dos componentes a CodigoCliente, cualquier modificación en uno de ellos, automáticamente modificará al otro.

Finalmente, habría que asignar los valores de las propiedades, tanto en el TDBEdit como en el TDBLookupComboBox. Veamoslo:

Primero en el TDBEdit. (el punto rojo nos indica la asignación)

inspector1

También modificamos los valores de las propiedades implicadas en el selector. La conexión con el componente ClientDataSet vinculado a la tabla clientes se hace a través de ListSource, ListField y KeyField.

inspector2

Esta podría ser una captura de la ejecución del código fuente que, aunque no resulte deslumbrante puesto que estamos compartiendo algo muy básico, al menos sí puede ser de utilidad para quien haya tenido dudas porque se enfrente por primera vez a montar estructuras similares. Descargad el código y comentáis lo que creáis oportuno.

formulario

Por hoy nada mas. Espero que os haya servido de ayuda.

La parte II, que posiblemente cerrará estos comentarios,  nos valdrá para reconocer algunas carencias del componente y ahondar en estas reflexiones que compartimos.

3 comentarios sobre “TDBLookupComboBox al escenario (Parte I)

  1. Que tal Salvador

    Muy interesante artículo, siempre es grato ver como otros colegas abordan temas que a simple vista son muy básicos (para los que saben) pero que te proporcionan las bases para entrar en cuestiones mas profundas.

    Te felicito y agradezco tus aportes que siempre son muy ilustrativos y de fácil seguimiento.

    Saludos

  2. Hola Eliseo:

    🙂

    Aprovecho tu mensaje para felicitar por la victoria de México frente a Francia (2-0). Una gran parte de compañeros que visitan estas páginas proceden de esa tierra grande y hermosa y se que algunos habrán pegado saltos de alegría después del partidazo.

    Wow… ¡Muy buen partido!!!!!!!

    Los españoles estamos un poco decaídos con la derrota, pero confiamos que nuestra selección reaccione.

    Recibe un saludo,

    Salvador

  3. Hola Salvador,

    Pues si, en las estadísticas la victoria de México a Francia hizo historia, pero habrá que tener los pies bien puestos en tierra, Francia no es la misma del último Mundial y sin demeritar el buen juego que se le vió al equipo mexicano, Francia no dió mucha batalla. De cualquier forma se sumaron 3 puntitos excelentes y al final es lo que cuenta, nos colocaron en la antesala de los octavos de final 🙂

    En cuanto a tu selección, no dudo que España levante sin problema, tiene un excelente plantel para ello (“aca entre nos…” Es uno de mis favoritos para ganar el mundial junto con Brasil y Alemania).

    Por otro lado, he estado leyendo periódicos españoles y veo cierta “pelea” entre españoles y franceses jejeje.

    Salud OS

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s