Data Binding en JDAL Swing
Introducción
El término Data Binding se refiere al concepto de procesar las entradas de datos desde la interfaz de usuario a los modelos del dominio de la aplicación de forma dinámica y más o menos automática. JDAL Swing soporta el binding de los controles Swing mediante la interfaz Binder desde la versión 1.0 y la interfaz ControlAccessor desde la versión 1.1.3.
La versión estable actual de jdal-swing 1.2.0 permite escribir aplicaciones swing sin necesidad de escribir ningún codigo de binding de datos. No importa lo complejas que sean la interfaces gráficas de usuario, Ud. sólo tendrá que preocuparse de crear el formulario y llamar en algún momento al método autobind().
Puede ver algunos ejemplos de código de formularios que utilizan jdal-swing en el paquete info.joseluismartin.gefa.ui de la aplicación Gefa (Gestión y Facturación) que he iniciado recientemente y se distribuye bajo licencia GPL.
Práticamente todos los formularios siguen el mismo patrón:
- Extienden AbstracView con el modelo sobre el que el formulario respaldará los datos.
- Llaman al método autobind() en el método init()
- Crean el formulario en el método buildPanel() con la ayuda de un BoxFormBuilder
Si el modelo de respaldo del formulario se persiste en base de datos, jdal-core también puede hacer ese trabajo por Ud. Anote el modelo con Hibernate o JPA y declare el servicio jdal para ese modelo en el contexto de la aplicación.
Sigue una pequeña descripción del funcionamiento interno del sistema de binding que utiliza jdal-swing. Aunque realmente no es necesario conocerlo para utilizar la librería, si le será útil en el caso de que necesite utilizar un componente swing que no esté soportado. En ese caso únicamente necesita crear un ControlAccesor para el componente (tal y como se ha hecho en JCalendarControlAccessor) y registrarlo en el contexto de la aplicación.
Binder
La interfaz Binder añade dos métodos a ModelHolder que permiten mover los datos entre el control de la interfaz de usuario y el modelo del dominio:
- update(): Actualiza el modelo a partir de la información del control.
- refresh(): Actualiza el control a partir de la información del modelo.
JDAL delega la obtención del Binder apropiado a la fábrica BinderFactory que contiene un FactoryMethod que asocia un Binder con la clase Java del componente.
La interfaz PropertyBinder es una especialización de Binder que permite asociar un control a una propiedad de un modelo.
Finalmente la clase CompositeBinder representa un agregado de PropertyBinders que comparten el mismo modelo.
Salvo que quiera Ud. extender la librería o bien, utilizar en su aplicación directamente el sistema de Data Binding proporcionado por JDAL, no necesita utilizar directamente estas clases. El Template AbstractView le proporciona el soporte necesario para crear interfaces de usuario Swing con binding automático entre controles y modelos del dominio.
ControlAccessor
La interfaz ControlAccessor define un contrato para acceder a los controles de la interfaz de usuario de forma genérica, es decir, sin el conocimiento específico del tipo de control o componenente. ControlAccessor define cuatro métodos:
- Object getControlValue(): Obtiene el valor que contiene el control, por ejemplo un String para un JTextField o un modelo del dominio para un View.
- void setControlValue(Object value): Establece el valor de control a value.
- void addControlChangeListener(ControlChangeListener listener): Añade un Listener que será notificado cuando se produzca un cambio en el valor que contiene el componente.
- void removeChangeListener(ControlChangeListener listener): Elimina un ControlChangeListener añadido anteriormente.
De forma similar a los Binders, JDAL delega la obtención del ControlAccessor apropiado para cada componente a una fábrica, ControlAccessorFactory cuya implementación por defecto, ConfigurableControlAccessorFactory utiliza un Map<Class,ControlAccessor> para asociar cada ControlAccessor a una clase Java.
Dada la similitud entre Binders y ControlAccessors, JDAL contiene una fábrica de Binders a partir de la fábrica de ControlAccessors ControlAccessorBinderFactory de modo que basta con implementar la interfaz ControlAccessor para un componente para disponer también del Binder asociado. No obstante, JDAL provee implementaciones de ControlAccessors y Binders tanto para los componentes Swing como para las implementaciones de la interfazView. El motivo es que la introducción de la interfaz ControlAccessor es más reciente y las implementaciones de los Binders se mantienen en la librería por motivos de compatibilidad. La recomendación actual es implementar únicamente los ControlAccessors para sus componentes y obtener los Binders mediante ControlAccessorBinderFactory.
AbtractView
AbstractView es la clase plantilla (Template, GoF) principal para la creación de interfaces de usuario Java que proporciona JDAL. 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, normalmente en el método init() de AbstractView
- Automático: En modo automático, AbstractView realizará la asociación entre controles y propiedades del modelo del dominio 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, normalmente en el método init().
Además del binding, AbstracView proporciona soporte para las siguientes funcionalidades:
- Activar/Desactivar todos los controles del la vista: Basta con llamar al método void enableView(boolean value) para activar o desactivar los controles sobre los que existe un binding.
- Validación del modelo: Actualmente únicamente se soporta la interfaz org.springframework.validation.Validator.
- 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 a rojo y añade un tooltip al componente con información sobre el error.
- 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.
En la mayoría de los casos, sólo necesitamos preocuparnos de crear el componente que contendrá los controles de la interfaz gráfica mediante la implementación del método abstracto JComponent buildPanel() y configurar un validador para disponer de un formulario Swing con todas las funcionalidades anteriores.
AutoBinder
La clase AutoBinder le permite utilizar el binding automático que proporciona JDAL sobre cualquier formulario sin imponer la necesidad de que el formulario extienda de AbstractView.
AutoBinder binder = new AutoBinder(view, model); binder.update(); ... binder.refresh()
JSR-303
Desde la versión 1.2.0 jdal-swing soporta validación mediante las anotaciones de código JSR-303. Debido a que jdal-swing soporta validación mediante la interfaz Validator de springframework y que desde la versión 3 spring soporta validación mediante JSR-303, utilizar JSR-303 con jdal es inmediato y basta con seguir los siguientes pasos:
- Configurar un bean LocalValidatorFactoryBean de spring en el contexto de la aplicación:
<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean" />
- Injectar este bean como Validator en AbstractView.
Sample
El ejemplo consiste en un formulario que permite editar connexiones a una base de datos. Utilizaremos la clase DBConnection como modelo y la clase DbConnectionForm como formulario. El único código de binding de datos que necesitamos la llamanda a autobind() en el método init() de DBConnectionForm.
AbstractView realizará todo el trabajo de binding por nosotros!.
DbConnection
/** * Model for database conections. * * @author Jose Luis Martin */ public class DbConnection { private static Log log = LogFactory.getLog(DbConnection.class); /** database driver info */ private Database database; /** host name */ private String host; /** database port */ private int port; /** database name */ private String dbName; /** user name */ private String user; /** database user password */ private String password /** * Test the database connection */ public boolean test() { boolean success = false; // Try to connect try { Class.forName(database.getDriver()); Connection conn = DriverManager.getConnection(dbName, user, password); conn.close(); success = true; } catch (Exception e) { log.error(e); } return success; } /** * @return the database */ public Database getDatabase() { return database; } /** * @param database the database to set */ public void setDatabase(Database database) { this.database = database; } /** * @return the host */ public String getHost() { return host; } /** * @param host the host to set */ public void setHost(String host) { this.host = host; } /** * @return the port */ public int getPort() { return port; } /** * @param port the port to set */ public void setPort(int port) { this.port = port; } /** * @return the name */ public String getDbName() { return dbName; } /** * @param name the name to set */ public void setDbName(String dbName) { this.dbName = dbName; } /** * @return the user */ public String getUser() { return user; } /** * @param user the user to set */ public void setUser(String user) { this.user = user; } /** * @return the password */ public String getPassword() { return password; } /** * @param password the password to set */ public void setPassord(String password) { this.password = password; } }
DbConnectionForm
/** * Database Connection Form * * @author Jose Luis Martin */ public class DbConnectionForm extends AbstractView<DbConnection> implements ActionListener { // Swing controls. We are using autobinding abstract view feature, so // we need to use the model properties as control names. private JComboBox database = new JComboBox(); private JTextField port = new JTextField(); private JTextField host = new JTextField(); private JTextField dbName = new JTextField(); private JTextField user = new JTextField(); private JPasswordField password = new JPasswordField(); private JButton test; private JLabel testResult = new JLabel(" "); private List<Database> databases = new ArrayList<Database>(); /** * @param dbConnection */ public DbConnectionForm(DbConnection dbConnection) { super(dbConnection); test = new JButton(getMessage("DbConnectionForm.test")); test.addActionListener(this); } /** * Init method, called by container after property sets. */ public void init() { // Call autobind to create control bindings. // This is the only bindign code that we need ! autobind(); } /** * Override build panel and create the JComponent that contain the view controls. */ @Override protected JComponent buildPanel() { BoxFormBuilder fb = new BoxFormBuilder(FormUtils.createTitledBorder(getMessage("DbConnectionForm.title"))); fb.setDebug(true); fb.row(); fb.startBox(); fb.setHeightFixed(true); fb.row() fb.add(getMessage("DbConnectionForm.database"),database); fb.row(); fb.add(getMessage("DbConnectionForm.host"),host); fb.row(); fb.add(getMessage("DbConnectionForm.port"), port); fb.row(); fb.add(getMessage("DbConnectionForm.dbName"), dbName); fb.row(); fb.add(getMessage("DbConnectionForm.user"), user); fb.row(); fb.add(getMessage("DbConnectionForm.password"), user); fb.endBox(); fb.row(); fb.startBox(); fb.row(); box.add(c); fb.row(); box.add(test); fb.row(); box.add(testResult); fb.endBox(); return fb.getForm(); } /** * Test database connection and show success/failed message */ public void actionPerformed(ActionEvent e) { if (getModel().test()) { testResult.setText(getMessage("DbConnectionForm.success")); } else { testResult.setText(getMessage("DbConnectionForm.failed")); } } /** * @return the databases */ public List<Database> getDatabases() { return databases; } /** * @param databases the databases to set */ public void setDatabases(List<Database> databases) { this.databases = databases; } /** * Main test method to show the form * @param args none */ public static void main(String[] args) { ViewDialog<DbConnection> d = new ViewDialog<DbConnection>(); DbConnectionForm dbf = new DbConnectionForm(new DbConnection()); dbf.setControlAccessorFactory(new ConfigurableControlAccessorFactory()); dbf.init(); // wrap the view in a ViewDialog to show it. ViewDialog will call view.update() on // on accept and dispose on cancel. d.setView(dbf); d.init(); d.setVisible(true); } }
Más Información
Puede obtener más información sobre JDAL en http://www.jdal.org (inglés) o consultar la documentación de JDAL en esta misma web que describe la aplicación de ejemplo.
Si lo desea, puede realizar sus preguntas o comentarios en foro de soporte de JDAL, trataré de responderle tan pronto como me sea posible.