Os pido -antes que nada- disculpas por la ausencia, ya que han tenido que pasar algunas semanas, hasta que he podido retomar esta última parte de la quinta entrega, en la que se abordaba la implementación de la clase TRejilla. Ésta, como otras comentadas anteriormente, forman una muy reducida parte o colección, extraida del framework de los cursos que Ian Marteens ha estado impartiendo en el area de gestión de datos, y concretamente de Datasnap. Hemos sido muchos los programadores que hemos aprendido con sus cursos, directa o indirectamente, y aunque Ian desde hace tiempo centró su trabajo, a tenor de lo que hemos ido siguiendo en sus blogs, en la esfera de C# y Freya, lo escrito por él sigue siendo válido y aplicable en muchos aspectos, y muchos todavía sentimos agradecimiento hacia sus libros publicados, sus Caras Ocultas, escritos entonces en un contexto en el que existía menos documentación a nuestro alcance. Creo que se entiende que, para mi, iniciar la serie, era una forma de agradecerle esa aportación.
Como sabéis, la iniciabamos meses atras con la única y sana intención de compartir la experiencia de estar manteniendo una aplicación real basada en su framework . Si hacemos un poco de memoria, cerrábamos la parte A, anticipando el interfaz que publicaba la clase y dejábamos para esta entrada comentar la implementación.
No… No es que me sobre el tiempo ahora, 🙂 pero cada vez que abría el editor del blog, pendía como una espada de Damocles sobre mi conciencia, y susurraba a mi oído algo similar a:
-«Salvadooooor… estoy esperandote… y van pasando las semanas y sigo estando en la zona de borradores…!!!»
(voz tenebrosa)
=:-)
Tan espectral visión de la pereza humana ha sacudido mi aletargado espíritu…
Fuera ya de bromas, los últimos meses han supuesto una sobrecarga de trabajo bastante grande y a pesar de mi voluntad en mantener al día el contenido del blog y finalizar la serie, (porque siempre hay algo que contar y compartir ¿no creéis?), no siempre nos es posible atenderlo como quisiéramos y merece. Así que he dejado aparcados durante un par de días varios temas pendientes, personales y de mi trabajo, y heme aquí, intentando retomar el hilo de lo escrito ( hilo perdido y bien perdido).
Vamos a ver…
Lo primero que tenemos que tener en cuenta es que la clase TRejilla, nos debería proveer la funcionalidad básica para manipular los datos de forma sencilla. Eso es lo que siempre se ha buscado. Así que la clave, en este punto puede estar en valernos de Acciones, que podrán ser asignadas en tiempo de diseño durante la etapa de creación de los módulos de trabajo. Es decir, que nuestro trabajo consistiría básicamente en decidir a tenor de cada uno de ellos, cuales acciones van a quedar disponibles, en la etapa de diseño, sin tener que estar reinventando la rueda continuamente. Esta sería mas o menos la mecánica de trabajo para nosotros a este nivel, durante el desarrollo de nuestra aplicación: Creamos un módulo descendiente de la clase TBrowser, añadimos un componente TRejilla en su interior y ¡voila!, asignamos las acciones que vamos a permitir en aquellos elementos del interfaz que las requieran. Si a este punto añadimos que Ian, hacia formar parte del browser una ToolBar que puede ser receptor de las acciones, el trabajo se simplifica aun mas.
Recordemos la imagen en la que podía ver que elementos iban a formar parte del componente TRejilla:
Figura 1.
En el interior del componente TActionList, se habían creado las diferentes acciones (TAction) relacionadas directamente con la manipulación de los datos.
Figura 2.
Las cinco primeras acciones forman parte de las acciones standard predefinidas para manipulación de datos, existentes en el editor de acciones usado en tiempo de diseño . Marteens, se vale de las cinco primeras, primer registro, anterior, siguiente, ultimo y actualizar. El resto, buscar, insertar, borrar y modificar son creadas partiendo de una acción normal, no predefinida.
Esta es una imagen de las acciones predefinidas standard, que abarcan múltiples contextos, uno de ellos entre otros, el que hace referencia a datos.
Figura3.
La propiedad más importante que tenéis que cumplimentar en este caso (nos referimos a las cinco primeras) es la asignación del datasource de cada acción para que apunte al datasource del browser (dsBase). De esa forma, movernos a través de los registros del dataset no nos costará una sola linea de código, ya que la acción ya implementa la activación de la misma en función de la posición del registro activo y la acción propia del movimiento de navegación. Esto es aplicable a DataSetFirst, DataSetPrior, DataSetNext, DataSetLast, y DataSetRefresh, que veis añadidas en el contenedor LocalActions.
Centrémonos ahora en DataSetSearch, que representa la acción que nos permitirá definir qué registros formarán parte de la rejilla que va a visualizar el usuario de la aplicación. Habitualmente definimos estás acciones como búsquedas y suele ser habitual que nuestros usuarios demanden un gran numero de ellas siguiendo los más pintorescos y variopintos deseos (y necesidades). Aunque vamos a omitir esta parte en el ejemplo práctico que nos propondremos para finalizar la serie, en aras de simplificar el código, sí podemos comentar el razonamiento ingenioso que ha seguido Ian para habilitar la funcionalidad, ya que es muy interesante y didáctico.
RECETA PARA HABILITAR UNAS BÚSQUEDAS DE FORMA SENCILLA:
Una vez creada la acción (DataSetSearch) , necesitamos concretar y definir ese elemento clave y central, sobre el que van a centrar las búsquedas. Ian Marteens llamó a ese elemento TGeneradorSQL, que va a ser, algo tan básico como una función que nos devuelva un string, representando la cadena sql mediante la que obtenemos los resultados de la petición.
Esta es la declaración del tipo, previa lógicamente a la establecida por TRejilla
TGeneradorSQL = function : string of object;
Asi que definir una búsqueda, se acaba convirtiendo simplemente en dar respuesta en el modulo de navegación a varias de estas funciones.
Yo por ejemplo, en la unidad UActividades, donde el usuario del Centro visualiza que actividades quedan disponibles para suscribirse, escribo unas lineas de código tal que así:
function TActividades.CadenaSQL: String; begin Result:= 'SELECT dbo.Actividades.IDActividad, ' + ' dbo.Actividades.CodigoActividad, ' + ' dbo.Actividades.Nombre ' + 'FROM dbo.Actividades ' + 'order by CodigoActividad '; end;
No… 🙂 Ya suponéis que no me he roto demasiado la cabeza, siquiera para definir los parámetros, sino que en este caso, nos devuelve todas las actividades existentes (que no eran demasiadas) ordenadas por código de actividad.
¿Veis en la figura 1, que existe un popupmenu, que he tachado para indicaros que no iba a ser incluido?. Ese popupmenu respondía al nombre de SearchPopup, y era manipulado mediante código en tiempo de ejecución, para que contuviera cada una de las acciones de búsqueda (definidas previamente en cada consulta sql devueltas por las funciones TGeneradorSQL). Así que bastaba crear en la barra de acciones (toolbar) del browser, un botón nuevo, al que se iba a asignar la ejecución del código de la acción, (que de forma genérica, era capaz de obtener cual de los TMenuItems era el pulsado por el usuario y cargar el dataset con la consulta vinculada al mismo).
Ajá… parece complicado ¿no es así?. Sin embargo, en la práctica es sencillo. Tan sencillo como definir una linea como la siguiente, de código en el evento de creación del formulario Browser:
RejillaActividades.TipoBusqueda('Todos los registros', CadenaSQL, True);
procedure TRejilla.TipoBusqueda(const Comando: string; Generador: TGeneradorSQL; PorOmision: Boolean); var NewItem: TMenuItem; begin if TMethod(Generador).Data <> Owner then Exit; NewItem := TMenuItem.Create(Self); NewItem.Caption := Comando; NewItem.OnClick := DatasetSearch.OnExecute; if PorOmision then FUltimaBusqueda := NewItem; NewItem.Tag := Integer(TMethod(Generador).Code); SearchPopup.Items.Add(NewItem); end;
Si analizamos el nucleo del código vinculado al evento OnExecute de la acción DataSetSearch,
procedure TRejilla.DataSetSearchExecute(Sender: TObject); var D: TClientDataSet; Gen: TGeneradorSQL; begin D := dsBase.DataSet as TClientDataSet; if Assigned(D) then begin ...
TMethod(Gen).Data := Owner; TMethod(Gen).Code := Pointer(FUltimaBusqueda.Tag); D.Close; D.CommandText := Gen(); D.Open; end; end;
Ahora ya podemos entender porque se guardaba en el tag del componente TMenuItem, el puntero al punto de entrada de la función (del método), que nos permite de forma «mágica» ejecutar la función asociada (representada por la variable Gen) y asignar la consulta SQL que abrirá el dataset de la rejilla.
Y lo mas bonito de todo esto, es que a nosotros, como programadores nos queda en este esquema de trabajo centrarnos en lo realmente importante, que es definir que consultas va a necesitar el usuario para que sus busquedas sean funcionales y eficaces. Este será el esquema a seguir en este caso: Asignar el boton que va a contener la acción de busqueda en la toolbar. Una vez hecho esto, le asignamos a dicho botón el popupmenu a su propiedad Dropdown, que espera el componente TPopupMenu que debe ser mostrado al expandirse el menú. Simultaneamente, debe tomar el valor Style: tbsDropDown, para que al pulsar sobre el componente TToolButton, permita que se despliegue dicho menú de búsquedas automáticamente y pueda ser pulsado por el usuario (que habría ejecutado finalmente el código del evento OnExecute de la acción DataSetSearch).
Es imposible comentar todos los matices e inquietudes que nos puede abrir la observación de estas lineas, que os aseguro que son bastantes. En mi caso concreto, la primera vez que me enfrenté a un código similar, salvando las diferencias, fue intentando descifrar que hacía entre bastidores la clase TThread (definida en la unidad Classes.pas), allá por Delphi 6. Si haceis memoria, una de las series que acompañó a la Revista Síntesis, fue concretamente una en la que se abordaban los hilos de ejecución en Delphi 5. Meses mas tarde, comentaba en uno de los artículos que el código había sido modificado con la nueva versión (Delphi 6) y que se habían echo algunos cambios significativos. Creo que fue la primera vez que vi código en el que se veía la asignación del doble puntero Code/Data (aunque en aquel caso estaba definido como procedure of object, si no recuerdo mal).
TMethod = record Code, Data: Pointer; end;
Una vez, comentada la acción de busqueda, vamos a descifrar el resto de las acciones (DataSetInsert, DataSetUpdate y DataSetDelete):
{ TRejilla: acciones de actualización } procedure TRejilla.DataSetInsertExecute(Sender: TObject); var D: TClientDataSet; begin D := dsBase.DataSet as TClientDataSet; if Assigned(D) then ManipuladorDatos.Insertar(D); end; procedure TRejilla.DataSetDeleteExecute(Sender: TObject); var D: TClientDataSet; begin D := dsBase.DataSet as TClientDataSet; if Assigned(D) and ConfirmarBorrado then ManipuladorDatos.Eliminar(D); end; procedure TRejilla.DataSetUpdateExecute(Sender: TObject); var D: TClientDataSet; begin D := dsBase.DataSet as TClientDataSet; if Assigned(D) then ManipuladorDatos.Modificar(D); end;
El esquema utilizado en los tres casos es el mismo. Se obtiene una referencia al dataset a través del datasource dsBase existente en TRejilla y si la referencia obtenida es válida, ejecutamos el código aplicable a cada acción. Este código se corresponde con la implementación que hace la clase TRejilla de la interfaz IManipulacionDatos. Esta interfaz esta definida unas lineas antes que la propia clase y contendrá los 3 métodos, cada cual dando respuesta a las tres acciones básicas: modificar, insertar y eliminar. Una función, nos ayuda a obtener una referencia válida al método (recordad que ya comentábamos sobre este punto en la entrada anterior, por lo que podéis releer la parte A respecto a este punto).
function TRejilla.ManipuladorDatos: IManipulacionDatos; begin if not Supports(Owner, IManipulacionDatos, Result) then GetInterface(IManipulacionDatos, Result); end;
Siempre podéis opinar sobre este punto: elegir un interfaz o por el contrario hacer uso de la herencia. A nuestros efectos, declarar como virtuales los métodos y no haber usado la interfaz IManipulacionDatos, podría tener un efecto similar, en cuanto a su contenido y a que puedan ser sobrescritos en el descendiente, si cambia o se amplia la funcionalidad de ellos. Sin embargo, parece que la guía al considerar una u otra estrategia radica en la idea de la conveniencia de ligarse a una clase determinada y la oportunidad de obligar a que exista una relación de jerarquía sobre clases que pueden tener poco en común.
Y respecto a lo que hace ConfirmarBorrado es tan evidente que quizás sobren las lineas. No obstante, como en este caso Ian ha usado un cadena como recurso, vale la pena comentarlo y que pueda verse este punto.
resourcestring SConfirmacionBorrado = 'Va a eliminar este registro.'#13'¿Está seguro?'; function ConfirmarBorrado: Boolean; begin Result := MessageDlg(SConfirmacionBorrado, mtConfirmation, [mbYes, mbNo], 0) = mrYes; end;
Ummmm… 🙂 Aun así, desgraciadamente existen usuarios cazurros para los que estas advertencias son papel mojado y a pesar de que uno advierte que van a perder los datos, exclaman con aires de fatalidad:
-No se… salvador… Dije que sí pero realmente no quería borrar… ¿Que puedo hacer? ¿He borrado los datos?
¡Dios mío! ¡Cuantas veces he llegado a escuchar cosas similares!. 🙂 Para muchos de estos usuarios, blindar un programa para que no falle, llega a convertirse en un verdadero arte… jajaja pues uno tiene que entender que aunque no desean eliminar un registro, aun a pesar de pasar la vista sobre varias advertencias modales, son capaces de seguir adelante sin saber siquiera lo que han leído unos segundos antes… porque desgraciadamente nadie los lee… 🙂
Y respecto al evento de actualización de estado, en las acciones DataSetDelete y DataSetUpdate, han sido suficientes un par de lineas para saber si existen registros, por lo que nos basta asignar el método al evento en tiempo de diseño. Para el caso de insertar no importa esta consideración, y nos da igual que existan o no registros en el Dataset, aunque sí que podríamos incluir la condición Assigned(dsBase.DataSet).
procedure TRejilla.HayRegistros(Sender: TObject); begin TAction(Sender).Enabled := Assigned(dsBase.DataSet) and not dsBase.DataSet.IsEmpty; end;
Finalmente, la clase TRejilla también da respuesta al mensaje que genera el dbGrid que contiene en su interior, y que va a permitir editar o insertar sin tener que pulsar sobre los botones de la barra toolbar (mediante un doble click sobre el registro activo de la rejilla de datos), simplemente con el tratamiendo de ese mensaje, que originalmente procedía al redefinir el doble click en la clase TDBGrid. Eso era abordado igualmente en la parte A de esta quinta entrega.
procedure TRejilla.CMMostrarEditor(var M: TMessage); begin if Assigned(dsBase.DataSet) then if dsBase.DataSet.IsEmpty then DataSetInsert.Execute else DataSetUpdate.Execute; end;
No se si llegué a comentar entonces, que este punto hizo que perdiese varias jornadas de trabajo, sopesando la necesidad de que este mensaje, pudiera estar coordinado con el estado de las acciones DataSetInsert y DataSetUpdate también, pues era paradójico que una acción pudiera estar deshabilitada por motivos distintos de la cantidad de registros existentes en el dataset y siguiese activa mediante el acceso desde la recepción del mensaje, creando una contradicción evidente. Lo cual me llevó al final a añadir «algún» sistema que desactivara el mensaje como una propiedad mas del componente TRejilla.
Creo que podemos tomar un pequeño respiro antes de proseguir navegando sobre el código de la unidad. Vamos a concedernos unos minutos de reflexión y pensemos por un momento hacia donde nos esta llevando las consideraciones actuales:
- tengo una rejilla que me muestra un conjunto de registros.
- el usuario toma una decisión que afecta al futuro de uno de ellos.
- y el conjunto de registros debe ser solidario con dicha decisión…
Ummmmmmmmmmmm…. ¿No se ve todavía claro el tema?
Debo de estar algo espeso esta noche: Voy a plantearlo de otra forma…
¿Cuando el usuario afecta a dicho registro existe un solo conjunto de datos o son al menos dos…? 🙂 Si fuera uno solo, y existiese un único dataset, los cambios efectuados sobre el mismo serían instantáneos pero claro, tendríamos un serio problema en el momento en el que añadieramos detalles enlazados, pues entendemos que la existencia de estos detalles vinculados al registro maestro van a ser algo consustancial a su existencia y el tamaño de la memoria y del traspaso de los datos a través de la red un bien escaso, en un mundo donde nunca sabes a ciencia cierta, si esa cantidad de registros que va a albergar va a ser razonable o disparatada.
😦
Así que nos aparece otra alternativa: tener dos datasets. El primero ligado a la rejilla de datos, con la misión de representar el conjunto de datos que retorna la consulta del usuario. Y un segundo dataset parametrizado, ligado al registro que es afectado. A este segundo dataset, es al que iríamos enlazando los detalles, lo cual ya empieza a ser razonable. Cada vez que se desea modificar o crear un nuevo registro, se recupera la información asociada al mismo si es una actualización o bien se inserta un registro vacío, de ser una inserción, y se deja al usuario operar sobre él, en esa ventana modal que representará cada descendiente de TDialogo. Al cerrarse dicha ventana, se guardaran los cambios o se desecharan.
Volviendo a la sentencia sql que abría la tabla de actividades, mientras que la sentencia del dataset de la rejilla se cargaría mediante la instrucción:
SELECT IDActividad, CodigoActividad, Nombre FROM dbo.Actividades order by CodigoActividad
El segundo dataset en cambio, mostraría un código sql similar a:
SELECT IDActividad, CodigoActividad, Nombre FROM dbo.Actividades where IDActividad = :IDActividad
¿Quiere decir esto que ya no tenemos ningún problema…? 🙂
No… que va… la naturaleza del problema ha cambiado. Ahora tenemos que hacer creer a nuestro usuario que el primer dataset es el afectado porque de no ser así, si el usuario confirma los cambios y no sucede nada en la rejilla, será instantanea su queja:
– Hey… Salvador… me dijiste que la cerrar la ventana guardaría los cambios y sin embargo en la rejilla siguén igual…
Un consejo: No intentéis que lo comprenda el usuario… 🙂 Simplemente hacedle creer que la realidad ha cambiado. Si el registro es modificado puede valernos sobrescribir los valores del antiguo registro con los nuevos. El usuario creerá realmente que el cambio en la rejilla obedece a que muestra los valores actuales de la base de datos y no la memoria de su caché. Si el registro ha sido insertado, basta añadir el nuevo registro al dataset vinculado a la rejilla. Y finalmente, cuando un registro va a ser borrado, para que vas a dejar que abra el editor… bórralo directamente del primer dataset y confirma los cambios sin que el segundo dataset sepa nada.
Ya nos dice el refrán que Quien siempre dice la verdad, puede permitirse tener mala memoria… 🙂
Ahora ya estamos en condiciones de entender que sucede al ejecutar las acciones Insertar, Modificar y Eliminar. Veamos el código que Ian ha escrito para estos tres casos:
{ TRejilla: acciones de actualización } function TRejilla.Insertar(D: TClientDataSet): Boolean; begin if not Assigned(FEditor) then Result := False else begin D := AbrirDetalles(True); try D.Append; Result := FEditor.Ejecutar(Owner) = mrOk; finally CerrarDetalles; end; end; end; function TRejilla.Eliminar(D: TClientDataSet): Boolean; begin D.Delete; Result := D.ApplyUpdates(0) = 0; if not Result then D.CancelUpdates; end; function TRejilla.Modificar(D: TClientDataSet): Boolean; begin if not Assigned(FEditor) then Result := False else begin D := AbrirDetalles(False); try D.Edit; Result := FEditor.Ejecutar(Owner) = mrOk; if Result and Assigned(FDetalles) then CopiarRegistro(dsBase.DataSet as TClientDataSet, FDetalles); finally CerrarDetalles; end; end; end;
Os he remarcado en color rojo, verde y azul, los detalles de la implementación que pueden necesitar una aclaración adicional, ya advertida en las lineas anteriores. Vamos a ver cada uno de ellos
- FEditor representa una referencia a la clase TDialogo, ascendente de la que realmente va a instanciarse. Siguiendo el ejemplo en el que hacíamos referencia a las actividades del centro, deberíamos tener una unidad UActividades (descendiente de TBrowser), que alberga en su interior una instancia de TRejilla. También tenemos una unidad UActividad (descendiente de TDialogo) que va a ser enlazada al manipulador de datos para que pueda conocer cual de las instancias debe crear al editar o insertar. Esto supone que en el evento de creación de UActividades, existirán algunas lineas que deberán configurar el componente TRejilla para que actúe correctamente. Os pongo unas lineas de lo que podría ser el evento OnCreate() para la clase TActividades.
procedure TActividades.FormCreate(Sender: TObject); begin inherited; Application.CreateForm(TActividadesSQLDatos, ActividadesSQLDatos); Application.CreateForm(TActividadesDatos, ActividadesDatos); RejillaActividades.Detalles:= ActividadesDatos.cdsActividad; RejillaActividades.Editor:= TActividad; // <<<------ !!!!!! RejillaActividades.TipoBusqueda('Todos los registros', CadenaSQL, True); RejillaActividades.Abrir(CadenaSQL); end;
- FDetalles, representa en cambio una referencia a lo que hemos podido determinar como segundo dataset. Le estamos indicando cual de ellos contiene el registro que va a ser afectado y como podreis ver en las dos siguientes métodos, en el caso de que esta asignación pueda no existir, se presupone que nuestro deseo es que sea afectado el primer dataset, es decir, apuntaría al datasource dsBase existente en nuestra Rejilla.
- AbrirDetalles, CerrarDetalles y CopiarRegistro: Todos tienen en común en que giran alrededor del campo FDetalles, con la diferencia de que los dos primeros hacen referencia a la apertura y cierre del dataset mientras que CopiarRegistro, se centra en un momento posterior a la edición, una vez aceptados los cambios, momento en el que nos es necesario engañar al usuario y hacerle creer que la rejilla de datos contiene las modificaciones. Si os fijais, solo en el caso de que el campo FDetalles haya estado asignado, se procede a copiar el valor contenido en dicho registro con destino al dataset al que apunta la rejilla. 🙂 Un truco sucio por parte de Ian pero muy efectivo… jajaja ¡Uno llega a disfrutar viendo cosas como estas…! Como es relativamente fácil la implementación de CopiarRegistro, sabiendo que tan solo existen dos parametros Origen y Destino, nos vamos a centrar directamente en conocer qué hacen AbrirDetalles y CerrarDetalles, no obstante, recordad que CopiarRegistro deberia tener en cuenta que una vez hecho el post en el dataset destino, se haría necesario llamar a MergeChangeLog, para aceptar los cambios y que nuestro Delta contega únicamente la nueva versión y no sean reenviados nuevamente, o bien invocar a CancelUpdate para deshacerlos de haber existido algun error. Este es el código que se implementa para dar respuesta a la apertura y cierre del segunda dataset.
function TRejilla.AbrirDetalles(Insertar: Boolean): TClientDataSet; var I: Integer; f: TField; begin Result := FDetalles; if not Assigned(Result) then Result := dsBase.DataSet as TClientDataSet else begin for I := 0 to Result.Params.Count - 1 do begin with Result.Params[I] do if Insertar then Value := -1 else begin f:= Nil; try f:= dsBase.DataSet.FindField(Name); except ; end; if f <> Nil then Assign(dsBase.DataSet.FieldByName(Name)); end; end; Result.Open; end; end; procedure TRejilla.CerrarDetalles; begin if Assigned(FDetalles) then FDetalles.Close; end;
La clave para enteder que sucede al ejecutar AbrirDetalles es no perder de vista que siempre van a existir esos dos DataSets. Si entendeis que esto es así, resultará evidente que solo cuando estamos editando busquemos en los parámetros del mismo que campos deben ser encontrados en el primer dataset. Volved ahora la vista hacia el parámetro IDActividad, unas lineas mas arriba.
SELECT IDActividad, CodigoActividad, Nombre FROM dbo.Actividades where IDActividad = :IDActividad
:IDActividad (la clave primaria de la tabla) va a ser buscada recorriendo el array de campos comparando sus nombres con el parametro. Esta linea en AbrirDetalles nos lo dice:
f:= dsBase.DataSet.FindField(Name);
Así que, el hecho de que los parámetros y los campos tengan el mismo nombre no es casual. Tened en cuenta de que es una búsqueda donde se tienen en cuenta inclusive mayúsculas y minúsculas por lo que es necesario estar atento a este detalle. En el caso de que estemos insertando, nos basta asignar el valor -1, valor inexistente en el dominio de valores de las claves primarias de la tabla y que nos asegura que previamente no exista ningún registro en el Dataset.
Y ya para acabar, resta ver el método Abrir, usado para que nuestra rejilla de datos muestre los registros, y el destructor Destroy, que finalmente cierra el dataset. El fragmento mas significativo de este procedimiento es la asignación de la consulta dinámica que debe ejecutarse, representada por la función Generador que devolverá la cadena SQL.
procedure TRejilla.Abrir(Generador: TGeneradorSQL);
var D: TClientDataSet; begin D := TClientDataSet(dsBase.DataSet); if Assigned(D) then begin D.Close; if Assigned(Generador) then D.CommandText := Generador() else D.CommandText := ''; D.Open; end; end; destructor TRejilla.Destroy; begin if Assigned(dsBase.DataSet) then dsBase.DataSet.Close; inherited Destroy; end;
Os tengo que robar unos minutos más antes de despedirnos, con un comentario que hace referencia a código que no hemos visto aquí pero que está relacionado con la funcionalidad del manipulador de datos. Me explico: en alguna de las entradas anteriores, compartía con vosotros que el componente TRejilla también podía ser usado en el interior de la ventana modal, puesto que en la vida real, era posible y natural que una tabla detalle, a su vez, fuera maestra de otros detalles, y pudiera darse el caso de que vieramos la necesidad de que fueran editados simultaneamente. En la entrada primera de la serie incluiamos una imagen de un ejemplo de este punto.
Hay cambios pues, en ese caso, que deberían contemplarse y que pueden pasar desapercibidos. Por ejemplo, la ejecución del destructor Destroy en la rejilla, podría cerrar el Dataset asociado, ya que el formulario modal habitualmente puede ser destruido también. Esto habría que evitarlo en el caso de hacer uso de ese esquema. Y de la misma forma, convendría demorar el Applyupdates al momento en el que sea cerrada la ventana modal principal. En la imagen se puede ver claramente ésto: Cualquier modificación sobre la rejilla de faltas de Asistencia no debería ser consolidada en cuanto no sea aceptada la ventana modal del beneficiario, que es la que realmente acepta los cambios. Y si hablamos sobre el borrado de los registros sucede algo similar. Puede llegar a existir, por ejemplo, una acción adicional a la existente para el borrado (DataSetDelete) que no aplique cambios. La podríamos llamar DataSetDeleteEnDetalle, y nos permitiría borrar en los detalles pero no aplicar los cambios en tanto no fuera cerrada la ventana modal principal.
Bueno… ya os he dado la lata demasiado tiempo. Hasta aquí hemos compartido el framework, quedando a vuestra disposición todas las piezas para que puedan ser encajadas. Como complemente a estas entradas, me gustaría añadir un video donde se pueda ver visualmente como se compone este puzzle pero os confieso que voy muy ajustado de tiempo y que pueden pasar bastantes semanas antes de que lo pudiera grabar. Ni idea…
Como siempre os digo, espero que estas entradas os puedan servir de algo y que os ayuden, ya que no tienen otro fin. Y como hago en cada una de ellas, os animo a recabar mas información sobre los cursos de Ian Marteens, en el enlace a su web, que también figura en la sección de enlaces de la barra lateral del blog.
Me resta darle las gracias a Ian por su generosidad, al aceptar que compartiera esta experiencia con la comunidad, como forma de aportar mi pequeño grano de arena.
Un saludo a todos.
Deja una respuesta