Se podría decir que, al iniciar esta tercera parte de la serie, en la que vamos a razonar sobre el framework de los cursos de Ian Marteens, nos introducimos en una de las areas más bonitas del mismo, donde los razonamientos formales y la abstracción se destacan sobre otros aspectos más mecánicos y menos atractivos (visto ésto como desarrolladores). No se si coincidireis o no, pero no existe demasiado «mérito» en hacer la asignación de una propiedad ni resulta algo demasiado creativo, salvo que lo vivamos desde la perspectiva del creador de componentes. Más bien, forma parte del elenco de actividades repetitivas, que hacemos diariamente de forma mecánica.
Sin embargo, en este caso, establecer relaciones de herencia y uso entre las entidades, es un ejercicio estimulante y enriquecedor. Marteens inicia su periplo al definir una clase base (TAncestro), en la cuspide de la jerarquía que define el dominio de la aplicación. Y conviene decir esto ya que TAncestro no nace de la nada puesto que hereda el comportamiento de la clase TForm, de la cual desciende. Lo cual, nos permite disponer de toda la funcionalidad de los formularios que vamos a manipular. ¿Y por que es conveniente que sea así? Pues como bien explica en los primeros parrafos del ejercicio 3 de la serie c :
«A partir de determinado tamaño de aplicación, se hace necesario organizar las ventanas de un proyecto de acuerdo a una jerarquia de herencia. El principal motivo es evitar tareas repetitivas cada vez que se añade una nueva ventana durante el desarrollo. Pero una vez que ya existen las ventanas prototipos, existen ventajas innegables para el mantenimiento de las mismas.»
La palabra clave aquí es Prototipo. Esa es la clave con la que deberiais quedaros. Es decir, buscamos prototipos que simplifiquen los procesos de creación de entidades, que permitan gastar el mismo esfuerzo en el proceso de construcción, a todos los niveles. No reinventar la rueda en cada nuevo formulario en todo aquello que va a ser comun a todos. Sí. Estoy seguro de que la mayoría coincidirá en esto, aunque luego el día a día, haga que no siempre podamos avanzar afianzando la base del edificio y tengamos que conformarnos en dar una mayor altura al mismo, limitandonos a constuir un piso mas.
Pero lo mejor, es verlo con dos imagenes en las que se muestran dos de las relaciones tradicionales que se pueden establecer entre distintas entidades: «es una» y «usa una». Una relación de herencia y otra de uso. En las imagenes van a aparecer las clases claves dentro de la estructura del framework y nos permitiran tener una visión global y amplia del mismo.
Relaciones de Herencia entre TAncestro, TBrowser y TDialogo.
Relaciones de Uso entre TBrowser y TRejilla.
Sobre la primera imagen, se definen las relaciones de herencia entre TAncestro y TDialogo, y TAncestro y TBrowser. En ambos casos, TAncestro es el ascendente de ambas clases, que servirán a su vez como ascendente de aquellas ya especificas de cada modulo. Mientras los descendientes de TBrowser nos ayudarán a seleccionar aquellos registros que van a ser requeridos por el usuario (valiendose de una instancia de TRejilla), los descendientes de TDialogo serán figuradamente la ficha sobre la que vamos a editar.
Sobre la última imagen, es obligado hacer un pequeña reflexión puesto que en la misma, puede verse en la parte izquierda como se ha colado sobre un recuadro definido por una linea verde, la clase TDialogo. Se ha colado de puntillas porque fijaos que el título que hemos utilizado es «Relaciones de Uso entre TBrowser y TRejilla» y de hecho, esta relacion entre TDialogo y TRejilla, no aparece expresamente definida en el curso de Marteens (o al menos no la he encontrado ni la recuerdo en estos momentos). Tampoco preocuparos demasiado si ahora mismo no lo veis porque mas adelante debería aparecer esta situación (la ficha de beneficiarios tenía direcciones y tambien suscripciones a actividades si recordais el esquema de nuestra bbdd). Serán necesarios unos pequeños cambios.
Siendo como es TRejilla, una clase especializada en la manipulación de datos, puede llegar a ser usada tambien para la edición de los detalles asociados al maestro de datos. Eso forma parte de la idea u objetivo deseado, que es la generación y uso de prototipos. Imaginemos la rejilla general de clientes, ¿de que nos serviremos para acceder a la ficha del cliente?. La respuesta es la que imaginais: de una instancia de TRejilla, que será la que nos lanzará el formulario de edición de registro. Pero… una vez dentro de esta: ¿por que no van a existir otros detalles? El cliente tendrá direcciones, pero tambien catalogos, tendrá tarifas, tendra un detalle de comisionistas. Otra instancia de TRejilla puede ser usada perfectamente siguiendo el mismo patrón, creando esquemas fáciles de memorizar de la estructura de cada módulo. En ese momento uno se da cuenta de que no necesita memorizarlos sino saber si ese módulo cumple con ese esquema general, lo cual facilitá el manteniento semanas o meses despues. Cada vez que construyas un modulo que se acople al prototipo deseado, menor coste de mantenimiento a medio y largo plazo.
Aunque no siempre, uno se puede ajustar, puesto que la convivencia de un modelo basado en la cache local, como es este, puede llegar a tener que convivir con la gestión de información real, capturada a través de consultas directas y puede ser problematico que ambos modelos convivan. Así que es posible que exista un pequeño porcentaje de formularios que requieran otro prototipo diferente, lo cual hasta cierto punto resulta lógico.
Tenemos que seguir avanzando. Otro paso mas. Allá vamos Ian…
Abrir el proyecto donde lo dejamos. En el IDE de Delphi hacemos «File->New->VCL Forms application – Delphi for Win32» y llamamos a ese nuevo formulario creado como «UAncestro.pas», guardandolo en la carpeta deseada. Podemos fijar el nombre del mismo como Ancestro.
Esta es sección de interface del modulo que Ian nos propone:
unit UAncestro; interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, Registry, ComCtrls; type IRegistroPropiedades = interface ['{365DAA3B-DADE-4FBD-9472-ED6F84D3B1DE}'] procedure Leer(R: TRegIniFile; const Seccion: string); procedure Guardar(R: TRegIniFile; const Seccion: string); end; TAncestro = class(TForm) procedure FormClose(Sender: TObject; var Action: TCloseAction); procedure FormCloseQuery(Sender: TObject; var CanClose: Boolean); procedure FormCreate(Sender: TObject); private protected function Modificado: Boolean; virtual; function Confirmar: TModalResult; virtual; procedure Guardar; virtual; procedure Descartar; virtual; protected procedure RegistroLeer; virtual; procedure RegistroGuardar; virtual; public class function Ejecutar(AOwner: TComponent = nil): TModalResult; class function Instancia: TAncestro; class function Mostrar: TAncestro; end;
En fin… vamos a aplicar el primer recorte y le damos un tijeretazo a lo que hace referencia a la persistencia (a la interfaz IRegistroPropiedades). Tambien hemos suprimido la implementación del evento OnCreate que tampoco es imprescindible. Ahora añadimos un componente a nuestro formulario de la clase TActionList, donde se va a establecer una nueva acción que llamaremos «acFinalizar» y generamos la respuestas al evento OnExecute de la misma. Para lanzarla le he definido un Shortcut en el inspector de objetos asignando la pulsación de las teclas (Shift+Ctrl+F12). El proposito lo veremos al comentar el evento OnCloseQuery del formulario. Esto es lo que nos queda:
unit UAncestro; interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, Registry, ComCtrls, ActnList; type TAncestro = class(TForm) ActionList1: TActionList; acFinalizar: TAction; procedure FormClose(Sender: TObject; var Action: TCloseAction); procedure FormCloseQuery(Sender: TObject; var CanClose: Boolean); procedure acFinalizarExecute(Sender: TObject); private protected function Modificado: Boolean; virtual; function Confirmar: TModalResult; virtual; procedure Guardar; virtual; procedure Descartar; virtual; protected procedure RegistroLeer; virtual; procedure RegistroGuardar; virtual; public class function Ejecutar(AOwner: TComponent = nil): TModalResult; class function Instancia: TAncestro; class function Mostrar: TAncestro; end;
Empezamos a trabajar en la implementación del módulo donde vamos a intentar comentar los razonamientos que encierran estas funciones.
Añadimos un recurso de cadena (esto será necesario para el diálogo de la acción).
resourcestring SForzarCierre = 'Si sigues adelante los cambios se perderán.'#13'¿Seguir? (S/N) ';
Las tres funciones de clase definidas para TAncestro en la parte publica (Mostrar/Instancia y Ejecutar) van a ser parte de los engranajes mas importantes en la transición entre formularios. Vamos a ver la implementación y la comentamos.
class function TAncestro.Ejecutar(AOwner: TComponent): TModalResult; begin if AOwner = nil then AOwner := Application.MainForm; with Create(AOwner) do try Result := ShowModal; finally Free; end; end; class function TAncestro.Instancia: TAncestro; var I: Integer; begin for I := Screen.FormCount - 1 downto 0 do begin TForm(Result) := Screen.Forms[I]; if Result.ClassType = Self then Exit; end; Result := nil; end; class function TAncestro.Mostrar: TAncestro; begin Result := Instancia; if Assigned(Result) then begin if Result.WindowState = wsMinimized then ShowWindow(Result.Handle, SW_RESTORE); Result.BringToFront; end else begin Result := Create(Application.MainForm); Result.Show; end; end;
Todo empieza cuando la ventana principal llama a la función de clase Mostrar. Pero ¿Quien llama a mostrar? Lo mejor es que lo veamos con la implementación de la acción que nos permite acceder al modulo de beneficiarios.
procedure TPrincipal.VerBeneficiariosExecute(Sender: TObject); begin TBeneficiarios.Mostrar; end;
¡Vale! TBeneficiarios es un de las clases que va a descender de TBrowser y dado que puede ejecutar el metodo definido en su ascendente, el resultado va a ser como sigue: primero se evalua la llamada a Instancia que va a devolvernos si la ventana ya se encuentra en ejecución (fijaros como Ian recorre todos los formularios de la matriz Screen para saber si ya se encuentra activo). Y depende del valor de esta referencia (si es nil o se encuentra asigndada) decidirá entre crear o reactivar el formulario respectivamente.
El resultado final es que tenemos un mecanismo comun a todos los formularios que sean descendientes de nuestro TBrowser, que van a ser la clase que nos muestre la rejilla de datos de trabajo.
Y con respecto a la función de clase Ejecutar, nos permitirá invocar la creación de los descendientes de TDialogo, que serán las fichas o formularios en los que el usuario podrá modificar o insertar registros. La clave es entender que la llamada a Create se ejecutará en el contexto de la clase descendiente, por lo que el resultado final es la creación del formulario y el consiguiente lanzamiento modal.
Para finalizar vamos a ver la implementación del bloque de procedimientos que permiten a Ian Marteens decidir que hacer con los cambios que ha hecho el usuario. Daros cuenta como el nivel de abstracción es grande. Se establecen las lineas de que va a hacer en cada caso pero no se preocupa a este nivel de su definición. Precisamente serán las clases descendientes las que tengan la responsabilidad de dar contenido a los procedimientos virtuales como Guardar o Descartar.
Quedaría como sigue:
procedure TAncestro.FormClose(Sender: TObject; var Action: TCloseAction); begin Action := caFree; end; procedure TAncestro.FormCloseQuery(Sender: TObject; var CanClose: Boolean); begin case ModalResult of mrNone: if Modificado then case Confirmar of mrYes: Guardar; mrCancel: CanClose := False; else Descartar; end; mrOk: Guardar; mrAbort: ; else if not Modificado or (Confirmar = mrYes) then Descartar else CanClose := False; end; end; procedure TAncestro.acFinalizarExecute(Sender: TObject); begin if MessageDlg(SForzarCierre, mtWarning, [mbOk, mbCancel], 0, mbCancel) = mrOk then begin ModalResult:= mrAbort; end; end; function TAncestro.Confirmar: TModalResult; var Mensaje: string; Botones: TMsgDlgButtons; begin if ModalResult = mrNone then begin Mensaje := '¿Desea guardar los cambios?'; Botones := mbYesNoCancel; end else begin Mensaje := '¿Desea abandonar los cambios?'; Botones := [mbYes, mbNo]; end; Result := MessageDlg(Mensaje, mtConfirmation, Botones, 0); end; function TAncestro.Modificado: Boolean; begin Result := False; end; procedure TAncestro.Guardar; begin end; procedure TAncestro.Descartar; begin end; end.
No se si estais de acuerdo conmigo, pero con cuatro lineas Marteens ha definido genialmente lo que hubieran podido ser tediosas repeticiones en cada uno de los modulos creados, de no haber utilizado este tipo de prototipos. Creo que cualquier programador disfruta cuando encuentra en su camino código que le hace pensar como hacer mejor las cosas y este módulo es un buen ejemplo de ello, sobretodo por su sencillez.
Nuestro usuario hizo doble click sobre la rejilla, o lanzó la ejecución de las acciones Editar o Insertar de menús o botones, y tras modificar los valores en el formulario, llega el momento inevitable en el que tiene que confirmar o descartar cambios. ¿Hacia donde pues deberiamos mirar para entender está pasando en ese bloque de código? Parece que la clave es evaluar la respuesta del usuario (saber si ha aceptado los cambios y bien los ha rechazado) y en función de esta respuesta actuar.
Eso sí, ese actuar, va condicionandose en funcion tambien de que existan modificaciones o no (nos lo diría la llamada a Modificado) ya que no es lo mismo que el usuario haya pulsado aceptar o descartar cambios. Tambien se previene que el usuario accidentalmente pueda perder los cambios hechos cancelando de forma fortuita, momento en el que un dialogo modal debería rescatarlo y darle la posibilidad de salvarlos adecuadamente.
Con respecto a la acción Finalizar, que no figura inicialmente en el framework, se corresponde con una «necesidad» descubierta durante el desarrollo real. Vereis, siempre partimos de situaciones felices en las que prevenimos los problemas que mas o menos vislumbramos en la etapa de analisis. En algunas ocasiones, si se producía una excepción durante el proceso de cierre de la ventana modal, era posible que esta no pudiera ser cerrada, ya que el usuario ni podía confirmar los cambios ni descartarlos. En una situación donde no existía forma de escapar, suponía un pequeño truco útil para que el usuario pudiera escapar del nudo, sabiendo que serían descartados los cambios hechos en la edición de la ficha, como mal menor. Por esa razón se reserva el tratamiento de la respuesta Abort que escapa y permite cerrar el formulario y descartar los cambios. Pero esto habría que verlo desde el enfoque de una solución de urgencia y no como un tratamiento a la excepción. De hecho, en una aplicación real podría existir un mecanismo de comunicación con el departamento de desarrollo para recibir esta incidencia (via mail, via log) y tratarla adecuadamente en una versión posterior.
Hasta aquí nos ha llevado el comentario de la clase TAncestro. El siguiente peldaño nos llevaría a estudiar las dos clases que descienden directamente de ésta, en la relación de jerarquía: TBrowser y TDialogo. Además, va a ser una buena excusa para ver de cerca otro detalle muy interesante, que son las interfaces. Seguimos en la primera semana del próximo mes.
Los que podais estar interesados en el curso de Marteens sabeis que podeis encontrar información pulsando en:
Siempre repito (creo que debo comentarlo al final de cada entrada de la serie) que estas entradas son meramente comentarios de un programador que ha seguido en su trabajo uno de los frameworks de los cursos y que está compartiendo esa experiencia con la comunidad. A mi, personalmente, me ha parecido fantástico y lo tengo en una valoración muy alta. No existe la intención de recrear todo el framework sino simplemente comentar los puntos que me parecen claves o interesantes del mismo, aunque logicamente, para poder verlos, le pedí permiso previamente a Ian, que no puso objeción, tal y como comenté en la primera de las entradas.
Hago y recalco este comentario para que no haya quien crea que Marteens participa de algun modo conociendo el contenido de estas entradas o dando el visto bueno a lo que voy escribiendo. En todo caso, yo como muestra de agradecimiento, dado que estas páginas no tienen caracter lucrativo sino todo lo contrario y es la unica forma que tengo de agradecercelo, estoy incluyendo al final un enlace a los cursos, con independencia de que piense que es un dinero bien gastado y se lo pueda recomendar abiertamente a los programadores que se inician en Delphi. Pero tampoco es algo que me haya pedido el, porque entre otras cosas tampoco lo necesita.
Hay muchos libros y documentación en los que se puede conocer la sintaxis del lenguaje o el uso de tal o cual componente o de tal o cual estructura… pero prácticamente ninguno donde puedas ver de cerca el desarrollo de gestión de datos con el detalle con el que Marteens los aborda.
Nada mas. Espero que pueda ayudar a alguien todos estos comentarios.
Deja una respuesta