AbstractView
Introducción
AbstractView es la clase plantilla ( Template , GoF) principal de JDAL para la creación de formularios Swing. Aunque es posible utilizar las diferentes funcionalidades que proporciona JDAL Swing de forma independiente, AbstractView le permite condensar el trabajo de desarrollo de formularios en una única tarea: Implementar el método buildPanel()
AbstractView soporta de forma automática las siguientes tareas comunes en el desarrollo de formularios Swing.
- Binding automático entre los controles de la interfaz de usuario y los modelos del dominio. Siguiendo unas reglas sencillas de nomenclatura, puede eliminar por completo en código de data binding en la construcción de formularios
- Validación del modelo: Actualmente únicamente se soporta la interfaz org.springframework.validation.Validator. No obstante esto es suficiente para utilizar anotaciones JSR-303 en los modelos.
- Procesamiento de errores de validación: La interfaz ErrorProcessor representa un contrato para el procesamiento de los errores de validación. La implementación por defecto BackgroundErrorProcessor cambia el color del componente y añade un tooltip con información sobre el error.
- Anidación o agrupación con otras vistas. AbstractView soporta la delegación de las operaciones hacia otras vistas mediante SubViews (Vistas sobre el mismo modelo) o Vistas sobre propiedades del modelo
- Estado de modificación de los controles: El método boolean isDirty() proporciona información sobre el estado de modificación de los controles de la vista.
- Activar/Desactivar todos los controles del la vista: Basta con llamar al método enableView(boolean value) para activar o desactivar los controles sobre los que existe un binding.
- Inicialización de los controles. AbstractView detecta anotaciones JPA en el modelo de respaldo y puede inicializar los datos de los controles de forma automática.
El proceso de construcción de un formulario basado en AbstractView puede resumirse en los siguientes pasos:
- Extender la clase AbstracView parametrizada con el modelo sobre el que el formulario respaldará los datos.
- Crear el JComponent que contendrá el formulario en el método buildPanel()
- Llamar en algún momento al método autobind(), generalmente mediante la configuración de un método de inicialización en el contenedor (init-method, @PostConstruct, @Autowire...).
Data Binding
AbstractView soporta actualmente dos formas de realizar el binding de datos entre los controles de la interfaz de usuario y los modelos del dominio:
- Manual: Por binding manual nos referimos a que es necesario llamar en algún momento al método bind(Object control, String propertyName) para asociar el control a una propiedad del modelo del dominio
- Automático: En modo automático, AbstractView realizará la asociación entre controles y propiedades del modelo mediante el método autobind() que asociará los controles de la vista a las propiedades del modelo del mismo nombre. Es decir, si el modelo contiene una propiedad name que cumpla la especificación Java Beans y se desea asociar dicha propiedad a un JTextField en la vista, bastará con declarar el campo JTextField name en la vista y llamar al método autobind() en algún momento.
En ambos casos, al crear un binding entre un componente Swing y una propiedad del modelo, se realizarán las siguientes acciones:
- El formulario recibirá notificaciones sobre cambios en el control mediante el método controlChange(). La implementación por defecto únicamente cambia el estado del formulario a dirty. Puede sobrescribir este método para personalizar el comportamiento. Recuerde llamar al método de la clase padre si desea mantener este comportamiento.
- Inicialización de los controles. Los controles Swing pueden inicializarse mediante la interfaz ControlInitializer. La implementación por defecto detecta anotaciones JPA y de JDAL.
Finalmente, AbstractView proporciona tres métodos plantilla que permiten personalizar el proceso de enlazado de datos para un caso determinado:
- doRefresh(): Se ejecuta durante el proceso de actualización de la vista a partir del modelo. Método refresh()
- doUpdate(): Se ejecuta durante el proceso de actualización del modelo a partir de la vista. Método update().
- onSetModel(): Se ejecuta al cambiar el modelo de respaldo. Método setModel().
Estructura Interna
AbstractView delega gran parte del trabajo en estrategias o clases de ayuda de JDAL. Es interesante echarles un vistazo ya que podría usarlas directamente o bien personalizar alguno de los comportamientos.
CompositeBinder
Realiza todo el trabajo de data binding entre los controles y modelos. Puede utilizarse de forma independiente fuera de la jerarquía de AbstractView. No es necesario extender clases propias de JDAL para disponer del sistema de enlazado de datos. Puede encontrar más información en el capítulo de enlazado de datos.
ErrorProcessor
Estrategia de procesamiento de errores de binding o validación. La implementación por defecto, BackgroundErrorProcessor Cambia de color el fondo de los controles que han fallado y añade un tooltip con la información del error.
BinderFactory
Fábrica de PropertyBinders CompositeBinder la utiliza para encontrar el Binder apropiado para cada tipo de control.
ControlAccessorFactory
Permite acceder a los controles Swing de forma genérica. Es decir, realizar operaciones sobre controles sin el conocimiento específico del tipo de control.
ControlInitializer
Estrategia de inicialización de los controles, normalmente con datos de los repositorios o de otras entidades. La implementación por defecto DefaultControlInitializer es capaz de detectar anotaciones JPA y la anotación @Reference de JDAL Core.
Ejemplo
Modelo
Crearemos una vista para la entidad Project que se muestra a continuación:
@Entity @Table(name="project") public class Project { @Id @GeneratedValue(strategy=GenerationType.AUTO) protected Long id; @NotEmpty protected String name = ""; @ManyToOne private Company customer; @NotNull private Double amount; @ManyToOne private Bid bid; private String description; @OneToMany(mappedBy="project") private Set<Work> works; @OneToMany(mappedBy="project", targetEntity=ProjectAttachment.class, cascade=CascadeType.ALL, orphanRemoval=true) private List<Attachment> attachments; @ManyToMany(cascade=CascadeType.ALL) private Set<User> users; // Accessors, Mutators and Behavior Omited.
Vista
El objetivo es crear el siguiente formulario para la edición de proyectos, que nos permite establecer el nombre, el cliente, la oferta que se hizo al cliente, el presupuesto inicial, una descripción, la relación de usuarios asignados al proyecto y por último una lista de archivos binarios adjuntados al proyecto con la siguiente especificación:
- El combo de ofertas estará enlazado con el combo de clientes de modo que sólo se muestren las ofertas que pertenecen al cliente seleccionado.
- El combo de clientes debe inicializarse con los clientes de la base de datos
- El componente de selección de usuarios permite editar los usuarios que están asignados al proyecto.
- Incluye un componente para editar los archivos adjuntados al proyecto.
A continuación sigue el código de la vista del formulario de proyectos utilizando AbstracView
- Inicializamos el formulario en el método init()
- Seguimos la convención de nombres para que se realice en binding de datos de forma automática.
- Incluimos una vista anidada a la vista principal para mostrar los ficheros adjuntos
- Generamos el JComponent en el método buildPanel() con la ayuda de BoxFormBuilder
- Como no tenemos garantías de que el modelo esté inicializado es necesario sobre escribir el método onSetModel() para evitar posibles excepciones de carga perezosa (LazyInitializationExceptions)
public class ProjectView extends AbstractView<Project> { private JTextField name = new JTextField(); private JComboBox customer = new JComboBox(); private JComboBox bid = new JComboBox(); private JTextField amount = new JTextField(); private JTextArea description = new JTextArea(5, 0); /** ManyToMany editor */ private Selector<User> users = new Selector<User>(); @Autowired /** inner view to show edit attachments */ private AttachmentView attachments; /** service to initialize entities */ private PersistentService<Project, Long> service; public ProjectView() { this(new Project()); } /** * @param model */ public ProjectView(Project model) { super(model); } /** * Init method, called by container after property sets */ public void init() { service.initialize(getModel()); // initialize model, ie drop Hibernate Proxies getControlInitializer().setInitializeEntities(true); / users.init(); // initialize Selector autobind(); // the binding code FormUtils.link(customer, bid, "bids"); // link customer and bids } /** * {@inheritDoc} */ @Override protected JComponent buildPanel() { // create a Box form builder to build the form BoxFormBuilder fb = new BoxFormBuilder(FormUtils.createTitledBorder(getMessage("Project.title"))); fb.row(); fb.startBox(); // first Box (name, customer, bid and amount fb.setFixedHeight(true); // fixed height on resizes fb.add(getMessage("Name"), name); fb.row(); fb.add(getMessage("Customer"), customer); fb.row(); fb.add(getMessage("Bid"), bid); fb.row(); fb.add(getMessage("Amount"), amount); fb.endBox(); // end first box fb.row(); fb.startBox(); // second box for description fb.add(FormUtils.newLabelForBox(getMessage("Description"))); fb.row(Short.MAX_VALUE); // let this row to be as higher as it can. fb.add(new JScrollPane(description)); fb.endBox(); // end second box fb.row(); fb.startBox(); // third box for user selector fb.row(); fb.add(new SeparatorTitled(getMessage("Users"))); fb.row(Short.MAX_VALUE); // let this row to be as higher as it can. fb.add(users); fb.endBox(); // end third box fb.row(); fb.startBox(); // last box for attachment fb.row(); fb.add(new SeparatorTitled(getMessage("Attachments"))); fb.row(Short.MAX_VALUE); // let this row to be as higher as it can. fb.add(attachments.getPanel()); fb.endBox(); // end last box. return = fb.getForm(); // return the JCompoenent } /** * {@inheritDoc} */ @Override public void onSetModel(Project project) { if (service != null) service.initialize(project); } /** * @return the service */ public PersistentService<Project, Long> getService() { return service; } /** * @param service the service to set */ public void setService(PersistentService<Project, Long> service) { this.service = service; } }
Configuración del Contexto
Como acabamos de ver, podemos escribir la vista del proyecto evitando el código de binding, prácticamente todo el código de inicialización de los componentes, el de validación y tampoco ha sido necesario añadir Listeners a los controles para recibir notificaciones de cambio en los valores. A cambio, deberemos dedicar algún tiempo a la configuración del contexto de Spring.
- Definir el servicio de persistencia para la entidad proyecto e inyectarlo en la vista
- Definir e inyectar los componentes de los que depende AbstractView
- Definir e inyectar el validador de Spring para soportar las anotaciones JSR-303
- Definir e inyectar las estrategias de procesamiento de errores (ErrorProcessors)
- Definir el diálogo que contendrá a la vista (projectEditor).
El nuevo namespace de jdal-swing simplifica considerablemente esta tarea respecto a versiones anteriores. Note que la mayoría de los beans mostrados a continuación sólo es necesario definirlos una vez por cada aplicación. Los beans específicos de este ejemplo son projectService, projectView y projectEditor
... <!-- Persistent Serivice for Projects --> <jdal:service entity="info.joseluismartin.gefa.model.Project"/> <!-- Generic context service --> <bean id="contextService" class="info.joseluismartin.logic.ContextPersistentManager" /> <!-- Configure factories and related swing dependences --> <swing:defaults /> <!-- JSR-303 Validation --> <bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean" /> <bean id="errorProcessor" class="info.joseluismartin.gui.validation.BackgroundErrorProcessor" /> <!-- Actions --> <bean id="acceptAction" class="info.joseluismartin.gui.action.ViewSaveAction" scope="prototype"> <property name="icon" value="/images/16x16/dialog-ok.png"/> <property name="service" ref="objectService"/> <property name="name" value="Accept"/> </bean> <bean id="cancelAction" class="info.joseluismartin.gui.action.DialogCancelAction" scope="prototype"> <property name="icon" value="/images/16x16/dialog-cancel.png"/> <property name="name" value="Cancel"/> </bean> <!-- Base definition for Editors --> <bean id="editor" abstract="true" > <property name="acceptAction" ref="acceptAction" /> <property name="cancelAction" ref="cancelAction" /> <property name="dialogWidth" value="800" /> <property name="dialogHeight" value="600" /> </bean> <!-- Base definition for Views --> <bean id="view" abstract="true" > <property name="controlAccessorFactory" ref="controlAccessorFactory" /> <property name="binderFactory" ref="binderFactory" /> <property name="controlInitializer" ref="controlInitializer" /> <property name="validator" ref="validator" /> <property name="errorProcessors"> <list> <ref bean="errorProcessor" /> </list> </property> </bean> <!-- Default Control Initializer --> <bean id="controlInitializer" class="info.joseluismartin.gui.bind.AnnotationControlInitializer"> <property name="persistentService" ref="contextService" /> </bean> <!-- Our Project View --> <bean id="projectView" class="info.joseluismartin.gefa.ui.ProjectView" parent="view" scope="prototype" > <property name="service" ref="projectService" /> </bean> <!-- Our Project Editor --> <bean id="projectEditor" class="info.joseluismartin.gui.ViewDialog" parent="editor" scope="prototype"> <property name="view" ref="projectView" /> </bean> ...
Ahora podemos obtener instancias del editor de proyectos mediante el bean "projectEditor".