Vamos a dar otro paso más…
En la tercera entrada de esta pequeña serie, nos centrábamos en la clase TAncestro, y pudimos ver como se convertía en la piedra angular del framework de Ian, de la que iba a heredar cualquier tipo de ventana que pudieramos pensar o necesitar.
Ian valora en sus ejemplos, dos arquetipos de ventanas diferenciados: el que representa a la clase TBrowser y el que representa a la clase TDialogo. Es su idea, dicho sea de paso, muy razonable, de acuerdo a su experiencia como experto y en ese intento de simplificar. Quizás en vuestro caso, mientras leéis estas lineas imaginéis otros contextos que hagan necesaria la creación de otros descendientes, de acuerdo a criterios mas selectivos, como por ejemplo pudiera ser la seguridad, donde ciertas ventanas pudieran necesitar un tratamiento especial. Nada es descartable y el framework debería ser una propuesta de trabajo que muchos van a adaptarse de acuerdo a sus necesidades. De hecho, os comento que en mi caso concreto y en lo que respecta a la funcionalidad del Framework, descarté algunas partes que no estaba utilizando, como era la posibilidad de arrastrar registros mediante drag&drop, que Ian sí introduce y pone en práctica. Respecto a las ventanas, al final no me fue necesario crear ningún tipo adicional aunque hubiera momentos en los que sí pude habérmelo planteado.
En el caso que nos ocupa existe un primer tipo, necesario para mostrar aquellos registros que el usuario demandará, de acuerdo a ciertos criterios no siempre sabidos de antemano. La idea es mostrar solo lo que el usuario necesita. El sentido común debería prevalecer siempre para seleccionar qué datos son los que realmente va a necesitar nuestro usuario, que por sistema demandará «todo». Es razonable darle lo que realmente necesita ¿no os parece?. Mi padre siempre me recordaba un refrán que decía que contra el vicio de pedir estaba la virtud de no dar. 🙂
También debería ser razonable (y ciertamente también discutible) que la ventana que va a mostrar los distintos registros no devolviera inicialmente ninguno y que fuera el usuario quien nos dijera que va a necesitar. Bueno… a nosotros no… me refiero a la aplicación. Marteens muestra un sistema muy sencillo de búsquedas que van a permitir que el usuario, mediante una elección, pueda satisfacer esa demanda de información.
De todo esto, y como resumen, queda la idea de que nuestro primer tipo, la clase TBrowser, va a servir para explorar los datos, permitirá navegar sobre esta información (algo usual en estas aplicaciones) y que nuestro usuario pueda también sobre ellos ejecutar determinados procesos en aras de «determinados» resultados. Cualquier caso que se nos ocurra puede ser oportuno: la lista de clientes que mas nos han comprado de cada provincia, aquellos que pertenecen a una provincia concreta, etc. Habitualmente pretenderá acciones conocidas por todos, como borrar, modificar o insertar, pero bien nos puede valer para extender a procesos que puedan recaer no ya sobre un registro concreto sino una colección de ellos. Esa es un poco la idea que justifica y da vida a la clase TBrowser.
Recordemos una imagen de la ventana, en la que existía un componente contenedor de acciones TActionList y una barra de botones TToolBar, que mostrará visualmente las distintas acciones disponibles para el conjunto de datos mostrados al usuario.
Esta es la imagen:
A continuación, mostramos el código, que realmente no va a extender el comportamiento de TAncestro, mas en la capacidad para imprimir el contenido de la información que mostrará la ventana. Todas estas lineas realmente son opcionales.
Ved el código que ha implementado Ian y comentamos:
[DELPHI]
unit UBrowser; interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, UAncestro, ComCtrls, ToolWin, ActnList, ImgList, UImpresion; type TBrowser = class(TAncestro, IImprimible) CoolBar: TCoolBar; ToolBar: TToolBar; protected procedure Imprimir; virtual; end; implementation {$R *.dfm} { TBrowser } procedure TBrowser.Imprimir; var I: Integer; Imp: IImprimible; begin for I := 0 to ControlCount - 1 do if Supports(Controls[I], IImprimible, Imp) then begin Imp.Imprimir; Exit; end; end; end. [/DELPHI]
Llegado este punto y dado que hemos utilizado la referencia en el uses a la unidad UImpresion, deberíamos también darle un mínimo de contenido para seguir adelante, con independencia de que no comentemos la implementación concreta que ha sido utilizada para imprimir los registros, puesto que esto es algo que podemos concretar de acuerdo a nuestras necesidades.
Como mínimo debería contener:
[DELPHI] unit UImpresion; interface uses Classes, SysUtils, Graphics, Printers, DB, DBGrids; type IImprimible = interface ['{267E5074-DF76-4ACE-ACCB-2BA6831F193A}'] procedure Imprimir; end; ... [/DELPHI]
Marteens se apoya y desarrolla una función con ambito global al modulo, con esta firma:
function CreateReport(const ATitle: string; AGrid: TDBGrid): IInforme;
que va a ser llamada por aquellos objetos creados que hagan uso del interfaz IImprimible, dando respuesta al contrato que concluye el procedimiento Imprimir.
Vosotros podéis crear vuestra propia función, pero debéis de tener en cuenta algo muy importante y es que para este nivel de abstracción, no conocéis que campos concretos van a existir y debéis actuar siempre bajo esa premisa… 🙂 Debería ser una función que en muchos puntos, lógicamente recorrerá estructuras apoyándose en las propiedades especificas para delimitar los dominios de las mismas. Un ejemplo para verlo con mas claridad: Podría ser una rejilla el objeto de nuestra atención y podría también ser la estructura genérica de columnas que contiene la rejilla, la que nos permitiera descubrir los distintos campos del informe. La propiedad Count de la clase TColumn, mantiene el total de columnas existentes en la matriz y cualquier bucle típico (for do, while do, etc..) recorrerá la matriz y nos permitirá acceder a la información deseada.
El como se monte este código es un tanto lo de menos. Ian simplifica al máximo y crea un informe genérico que nos pueda valer para cualquier rejilla que muestre datos al usuario. De esa forma, economizamos nuestro esfuerzo y nos despreocupamos de este tipo de detalles que suelen ser bastante ingratos y absurdos.
En la declaración de tipos anterior, nos aparece la declaración de la interfaz IImprimible, que consta de un único procedimiento, acompañada del CLSID o GUID, propios de una interfaz COM.
Volviendo a la idea comentada, yo acabé añadiendo unas rutinas que permitían exportar a Excel el contenido de cualquier información mostrada por las ventanas descendientes de TBrowser. ¿Qué tal crear otra interfaz IExportable, tal que pudiera considerar esta opción? Son decisiones que van a depender de vuestro contexto de trabajo para valorar que pueda ser necesarias o no lo sean. En cualquier caso, no resulta demasiado difícil descubrir que existen colgando de la red de redes distintas implementaciones que hagan precisamente esto. Así que pudiera ser una buena excusa para dar un vistazo y ver si puede ser usada para nuestro fin.
No perder de vista la implementación del método Imprimir que hace TBrowser. Este punto es clave.
for I := 0 to ControlCount - 1 do if Supports(Controls[I], IImprimible, Imp) then begin Imp.Imprimir; Exit; end;
Cumplimos nuestro contrato y además delegamos la impresión en cualquier objeto que soporte la interfaz IImprimible, contenido en nuestra instancia descendiente de TBrowser. Lógicamente hacemos algo de trampa. Nosotros no… Ian, que es mas pillo que bonico, que dirían en mi pueblo… 🙂 porque estamos suponiendo que solo existe un objeto que implemente la interfaz. Pero es nuestro supuesto y partimos de que no va a existir mas de una rejilla en cada explorador de datos. ¿Podría darse el caso de que nuestro explorador de datos tuviera un maestro y un detalle? Son cosas que nadie lo sabe salvo el desarrollador que tiene entre manos el proyecto y estudia el interfaz adecuado a esa información.
Supports, es una función implementada en el modulo SysUtils que nos permitirá saber si el objeto (en este caso concreto representado por la matriz Controls[I]), usa el contrato, permitiendo referenciar en la variable Imp de tipo IImprimible (del tipo de nuestra interfaz) una forma de acceder a la instancia y ejecutar el método en cuestión.
Si el control no declara la interfaz en alguno de sus ascendientes o en él mismo, la función lo ignorará. Si encuentra alguno, no buscará mas.
Y respecto a la segunda unidad, tampoco el código es excesivo. Esta es la imagen de la ventana que representa a TDialogo y el código presente en el módulo usado por Ian en el curso:
[DELPHI] unit UDialogo; interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, UAncestro, DB, DBClient, StdCtrls, ActnList, ImgList; type TDialogoClass = class of TDialogo; TDialogo = class(TAncestro) dsBase: TDataSource; BasePanel: TGroupBox; bnOK: TButton; bnCancel: TButton; procedure FormKeyPress(Sender: TObject; var Key: Char); protected function Modificado: Boolean; override; procedure Guardar; override; procedure Descartar; override; end; implementation {$R *.dfm} { TDialogo } function TDialogo.Modificado: Boolean; begin if not Assigned(dsBase.DataSet) then Result := False else with dsBase.DataSet as TClientDataSet do if Assigned(DataSetField) then Result := Modified else Result := Modified or (ChangeCount > 0); end; procedure TDialogo.FormKeyPress(Sender: TObject; var Key: Char); begin inherited; if (Key = #13) then begin Perform(WM_NEXTDLGCTL, 0, 0); Key := #0; end; end; procedure TDialogo.Guardar; begin if Assigned(dsBase.DataSet) then with dsBase.DataSet as TClientDataSet do if Assigned(DataSetField) then CheckBrowseMode else if ApplyUpdates(0) > 0 then Abort; end; procedure TDialogo.Descartar; begin if Assigned(dsBase.DataSet) then with dsBase.DataSet as TClientDataSet do if Assigned(DataSetField) then Cancel else CancelUpdates; end; end. [/DELPHI]
Este segundo tipo de ventana, responde a la edicion/inserción de datos de nuestro explorador, representado como ya hemos comentado por la clase TBrowser. El usuario seleccionará uno de los registros, y esta ventana (TDialogo), nos permitirá recoger esos cambios.
Marteens nos hace una propuesta donde TDialogo es modal. La pregunta sobre la bondad o maldad de ser así ya es otro cantar. Yo también encuentro mas sencillo este esquema, que trabajar sobre el supuesto de un formulario no modal. De hecho, no me he planteado para este tipo de aplicación un esquema distinto puesto que funciona bien, ya que le permites a tu usuario libertad a nivel del explorador de datos con la única condición de que concluya las operaciones. ¡No todo van a ser ventajas para nuestro usuario mimado. Lo cual, introduce cierta inquietud sobre el hecho mismo de considerar que puedan no existir las condiciones necesarias para que concluya el proceso de edición con éxito, lo cual suele ser salvado mayormente creando ventanas auxiliares que favorezcan cumplir esas condiciones. Pero claro… tiene que existir un limite en esa recurrencia.
Originalmente el curso trae los procedimientos Modificado, Guardar y Descartar, que eran tres de las acciones abstractas, ya comentadas en la entrada anterior. Creo recordar que cansado de añadir la implementación del evento OnKeyPress de cada formulario, acabé escribiendo la rutina con la idea de asignarla en el descendiente si me era necesario.
Respecto a los tres procedimientos, quizás lo veáis mas claro pensando que pueda ir ligada su invocación sobre una fuente de datos que apunta al registro maestro o haga referencia a uno de sus detalles. Dado que no sabe a ciertas, desde este punto el origen de la llamada, no puede mas que prevenir que sea el ClientDataSet que soporta la acción (representado en dsBase.DataSet) sea el maestro de datos, en cuyo caso no tendría asignado el campo DataSetField. De ser al contrario, si dsBse.DataSet apunta a una fuente de datos que representa un detalle del registro maestro, existiría esta asignación. Marteens actua en consecuencia. En este punto quizás podría seros util recordar una entrada que compartimos tiempo atrás y que hablaba de esto precisamente:
(Entradas: Por curiosidad I, II y III) Por curiosidad (Parte I) , Por curiosidad (Parte II) y Por curiosidad (y Parte III)
En los tres casos, Modificado, Guardar y Descartar hacen cosas distintas según el caso. Intentaremos comentar esto con mas detalle cuando abordemos la parte que hace referencia a datasnap.
Espero que estas entradas os puedan servir de algo y que os ayuden. Y como hago en cada una de ellas, os animo a recabar mas información sobre los cursos de Ian, en el enlace a su web, que también figura en la sección de enlaces de la barra lateral del blog.
Hola Salvador.
A mi personalmente me ha resultado especialmente interesante la parte de los Interfaces.
Un saludo.
Me gustaMe gusta