JDAL - Aplicación de Ejemplo - Capa de Presentación
Para realizar la capa de presentación utilizaremos jdal-swing. Antes de empezar, veamos como jdal resuelve los problemas comunes de la programación de este tipo de capas, es decir:
- Crear Vistas de los modelos para la visualización y edición de datos.
- Intercambio de datos entre los controles de la interfaz de usuario y los modelos de la aplicación.
- Validación de datos en la capa de presentación.
La librería proporciona para ello, (oh! sorpresa) Views, Binders y Validators. El objetivo principal de jdal-swing es hacer que este proceso sea lo más simple posible, incrementando la productividad en este tipo de desarrollos.
Views
La interfaz info.joseluismartin.gui.View<T> contiene las operaciones básicas que deberán implementar los formularios de edición de modelos.
- T getModel(): Devuelve el modelo asociado al formulario.
- String getName(): Devuelve el nombre asociado al formulario.
- setModel(T model): asigna un modelo al formulario.
- refresh(): Actualiza los controles leyendo los datos del modelo.
- update(): Actualiza el modelo leyendo los datos de los controles
- getPanel(): Obtiene el JComponent asociado al formulario.
- validateView(): realiza la validación del modelo.
- clear(): Limpia los datos del formulario a sus valores por defecto.
info.joseluismartin.gui.AbstractView<T> es una plantilla (Template, GoF) que incluye soporte para el binding automático de los controles a las propiedades del modelo, subviews y validación utilizando validadores de spring framework. La plantilla define cuatro callbacks de los que sólo estamos obligados a implementar uno, buildPanel()
- void onRefresh(): Se llama después de refrescar los controles de forma automática para permitir añadir operaciones en el refrescado que no estén soportadas por AbstractView
- void onUpdate(): similar a onRefresh() para los updates.
- onSetModel(): se llama para notificar cambios en el modelo.
- JCompnent buildPanel(): método abstracto que se debe implementar para construir el JComponent del formulario.
La aplicación declara los Views en fichero de configuración del contexto de spring applicationContext-view.xml. En este fichero se declara el método init() como método de inicialización de los beans que spring llamará justo después de haber inyectado las dependencias.
AuthorView
El formulario de autor nos permitirá añadir un autor desde el formulario de edición de libros. Está compuesto por dos cuadros de texto, uno para el nombre y otro para el apellido.
Para crear AuthorView.java extenderemos AbstractView<Author>, realizaremos el enlazado de los controles con el modelo en el método init() mediante el método bind() de AbstractView. Este método recibe como argumentos una referencia al control (en este caso JTextField) y el nombre de la propiedad que recibirá el valor del control en el modelo.
AbstractView encuentra el binder apropiado a partir de la clase de la propiedad del modelo y de la fabrica de binders (BinderFactory) que se inyecta desde el contexto de spring.
En el método buildPanel() crearemos un JComponent con ayuda de la clase BoxFormBuilder que nos sirve para crear Formularios tabulares utilizando java.swing.Box como contenedores.
No es necesario crear el diálogo ya que posteriormente utilizaremos la clase info.joseluismartin.gui.ViewDialog para obtener un JDialog de forma declarativa a partir de un View
public class AuthorView extends AbstractView<Author> { private JTextField name = new JTextField(25); private JTextField surname = new JTextField(25); public AuthorView() { this(new Author()); } public AuthorView(Author author) { setModel(author); refresh(); } public void init() { bind(name, "name"); bind(surname, "surname"); } @Override public JComponent buildPanel() { BoxFormBuilder fb = new BoxFormBuilder(); fb.add("Name: ", name); fb.row(); fb.add("Surname: ", surname); JComponent form = fb.getForm(); form.setBorder(FormUtils.createTitledBorder("Author")); return form; } }
Sigue la declaración del bean authorView en applicationContext-view.xml
<!-- Abstract bean definition for Views --> <bean id="view" abstract="true"> <property name="binderFactory" ref="binderFactory" /> <property name="messageSource" ref="messageSource" /> </bean> <!-- AuthroView --> <bean id="authorView" class="org.jdal.samples.library.ui.AuthorView" scope="prototype" parent="view" /> <!-- AuthorDialgo that save Author on AcceptAction --> <bean id="authorDialog" class="info.joseluismartin.gui.ViewDialog" scope="prototype"> <property name="view" ref="authorView" /> <property name="acceptAction" ref="acceptAction" /> <property name="cancelAction" ref="cancelAction" /> </bean> <!-- Generic AcceptAction for dialogs, save models using <Object> persistentService --> <bean id="acceptAction" class="info.joseluismartin.gui.action.ViewSaveAction" scope="prototype"> <property name="icon" value="/images/16x16/dialog-ok.png" /> <property name="service" ref="persistentService" /> <property name="name" value="Accept" /> </bean> <!-- Generic CancelAction for ViewDialogs, close dialog --> <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>
BookView
Para crear la vista asociada a un libro seguiremos la misma estrategia que para la vista del autor. Aunque esta vista es algo más complicada, en el procedimiento en esencia es el mismo.
En este caso es necesario cargar la lista de categorías en el JComboBox que utilizamos para seleccionar/mostrar la categoría de un libro. ListComboBoxModel es un ComboBoxModel que utiliza una Lista como contenedor.
También necesitamos una instancia de GuiFactory, un wrapper simple del ApplicationContext de spring para obtener una instancia del bean "authorDialog" con el objeto de permitir añadir un autor si el autor del libro no se encuentra ya en la base de datos.
Finalmente añadimos el auto-completado del combo que permite la selección del autor para cargar dinámicamente la lista de autores mediante el filtro de hibernate que declaramos anteriormente en la clase Autor. Basta crear una instancia de FilterAutoCompletionListener y asignarle el servicio de persistencia que debe utilizarse para obtener la lista en función del contenido del editor de JComboBox.
public class BookView extends AbstractView<Book> { private static final String ADD_ICON = "/images/16x16/list-add.png"; private JTextField name = new JTextField(); private JTextField isbn = new JTextField(); private JComboBox author = FormUtils.newCombo(25); private JCalendarCombo published = FormUtils.newJCalendarCombo(); private JComboBox category = FormUtils.newCombo(25); private String authorEditor = "authorEditor"; private GuiFactory guiFactory; private PersistentService<Category, Long> categoryService; private AuthorService authorService; public BookView() { this(new Book()); } public BookView(Book book) { setModel(book); } public void init() { bind(name, "name"); bind(isbn, "isbn"); bind(author, "author"); bind(category, "category"); bind(published, "publishedDate"); } @Override protected JComponent buildPanel() { // fill category combo with data from database. category.setModel(new ListComboBoxModel(categoryService.getAll())); // Add auto-completion to author combo, limit max results to 1000 and order data by surname FilterAutoCompletionListener acl = new FilterAutoCompletionListener(author, 1000, "surname"); acl.setPersistentService(authorService); author.setEditable(true); // Create a Box with author combo and add button Box authorBox = Box.createHorizontalBox(); authorBox.add(author); authorBox.add(Box.createHorizontalStrut(5)); authorBox.add(new JButton(new AddAuthorAction(FormUtils.getIcon(ADD_ICON)))); // Build Form with a BoxFormBuilder BoxFormBuilder fb = new BoxFormBuilder(); fb.add("Title: ", name); fb.row(); fb.add("Author: ", authorBox); fb.row(); fb.add("ISBN: ", isbn); fb.row(); fb.add("Published Date:", published); fb.row(); fb.add("Category", category); JComponent form = fb.getForm(); form.setBorder(FormUtils.createTitledBorder("Book")); return form; } private class AddAuthorAction extends AbstractAction { public AddAuthorAction(Icon icon) { putValue(Action.SMALL_ICON, icon); } public void actionPerformed(ActionEvent e) { ViewDialog dlg = (ViewDialog) guiFactory.getDialog(authorEditor); dlg.setModal(true); dlg.setVisible(true); if (dlg.getValue() == ViewDialog.OK) { getModel().setAuthor((Author) dlg.getModel()); refresh(); } } } // Getters and Setters ... }
BookFilterView
La última vista se corresponde al filtro de libros. Nuevamente asignaremos los controles a las propiedades del modelo en el método init() y crearemos el JComponent en el método buildPanel() con ayuda de la clase BoxFormBuilder.
En este caso sobre escribiremos el método doRefresh() de AbstractView para incluir la carga de la lista de categorías en cada operación refresh() del View ya que el filtro estará incluido en un componente cuya vida en ejecución es igual a la de la aplicación.
public class BookFilterView extends AbstractView<BookFilter> { private JTextField name = new JTextField(20); private JTextField authorName = new JTextField(20); private JTextField authorSurname = new JTextField(20); private JCalendarCombo before = FormUtils.newJCalendarCombo(); private JCalendarCombo after = FormUtils.newJCalendarCombo(); private JComboBox category = FormUtils.newCombo(20); private PersistentService<Category, Long> categoryService; public BookFilterView() { this(new BookFilter()); } public BookFilterView(BookFilter filter) { setModel(filter); } public void init() { bind(name, "name"); bind(authorName, "authorName"); bind(authorSurname, "authorSurname"); bind(before, "before"); bind(after, "after"); bind(category, "category"); } @Override protected JComponent buildPanel() { BoxFormBuilder fb = new BoxFormBuilder(); fb.add("Title: ", name); fb.add("Author Name: ", authorName); fb.add("Author Surname: ", authorSurname); fb.row(); fb.add("Category: ", category); fb.add("Published Before: ", before); fb.add("Published After: ", after); JComponent box = fb.getForm(); box.setBorder(FormUtils.createTitledBorder("Book Filter")); return box; } @Override public void doRefresh() { List<Category> categories = categoryService.getAll(); categories.add(0, null); category.setModel(new ListComboBoxModel(categories)); } // Getters and Setters... }
Con esta vista terminamos la programación Java del ejemplo. Queda aún la configuración del los listados de libros, autores y categorías antes de que podamos ejecutar la aplicación.
Tabla Paginable
PageableTable es JPanel que contiene un JTable, un PaginatorView, un ListTableModel y un PageableDataSource.
La clase ListTableModel es un TableModel que permite configurar fácilmente las columnas del la tabla mediante el fichero de configuración del contexto de spring. El método setComlumns(Columns columns) en ListTableModel nos permite especificar las columnas de la tabla de la siguiente forma:
<bean id="bookTableModel" class="info.joseluismartin.gui.ListTableModel" scope="prototype"> <property name="modelClass" value="org.jdal.samples.library.model.Book"/> <property name="columns"> <list value-type="info.joseluismartin.gui.ColumnDefinition"> <bean class="info.joseluismartin.gui.ColumnDefinition"> <property name="name" value="name"/> <property name="displayName" value="Name"/> </bean> <bean class="info.joseluismartin.gui.ColumnDefinition"> <property name="name" value="author"/> <property name="displayName" value="Author"/> <property name="sortProperty" value="author.name"/> </bean> <bean class="info.joseluismartin.gui.ColumnDefinition"> <property name="name" value="category"/> <property name="displayName" value="Category"/> <property name="sortProperty" value="category.name"/> </bean> <bean class="info.joseluismartin.gui.ColumnDefinition"> <property name="name" value="isbn"/> <property name="displayName" value="ISBN"/> </bean> </list> </property> <property name="usingActions" value="true"/> <property name="usingChecks" value="true"/> </bean>
En la definición de una columna, podemos especificar las siguientes propiedades:
- name: El nombre de la propiedad del modelo que esta columna representa.
- displayName: El título de la cabecera de la tabla
- editable: Verdadero si la columna es editable.
- width: El ancho preferido de la columna.
- sortProperty: El nombre de la propiedad que se utilizará para ordenar la columna. Por ejemplo 'author.name'.
- renderer: el TableCellRenderer que se configurará en el TableColumn
- editor: El TableCellEditor que se configurará en el TableColumn.
- clazz: La clase de los objetos que se muestran en esta columna.
La propiedad actions nos permite especificar RowActions, ie, acciones a ejecutar sobre los elementos de una columna, y la propiedad usingChecks permite especificar si se mostrará una columna de checks para permitir seleccionar filas en la tabla.
Table Panel
TablePanel es un JPanel con una tabla paginable, una botonera y un filtro, tal como se muestra en la siguiente figura:
La botonera contiene JButtons configurados con TablePanelActions que son simplemente Actions con una referencia al TablePanel que los contiene. La librería proporciona Actions para añadir, borrar, editar, mostrar/ocultar filtros y seleccionar filas.
El siguiente recuadro muestra la configuración del TablePanel que se utiliza en la aplicación para mostrar los resultados de la búsqueda de libros.
<!-- TablePanel abstract configuration -->" <bean name="tablePanel" abstract="true"> <!-- Default Actions for TablePanel --> <property name="actions"> <list value-type="java.awt.Action"> <bean class="info.joseluismartin.gui.table.AddAction" /> <bean class="info.joseluismartin.gui.table.SelectAllAction" /> <bean class="info.joseluismartin.gui.table.DeselectAllAction" /> <bean class="info.joseluismartin.gui.table.RemoveAllAction" /> <bean class="info.joseluismartin.gui.table.HideShowFilterAction" /> <bean class="info.joseluismartin.gui.table.ApplyFilterAction" /> <bean class="info.joseluismartin.gui.table.ClearFilterAction" /> </list> </property> </bean> <!-- Paginator for PageableTable --> <bean id="paginatorView" class="info.joseluismartin.gui.PaginatorView" scope="prototype"> <property name="firstIcon" value="images/table/22x22/go-first.png" /> <property name="lastIcon" value="images/table/22x22/go-last.png" /> <property name="nextIcon" value="images/table/22x22/go-next.png" /> <property name="previousIcon" value="images/table/22x22/go-previous.png" /> <property name="pageSizes" value="10,20,30,40,50,100,All" /> </bean> <!-- Pageable Table of Books --> <bean id="bookTable" class="info.joseluismartin.gui.PageableTable"> <property name="dataSource" ref="bookService" /> <property name="tableModel" ref="bookTableModel" /> <property name="paginatorView" ref="paginatorView" /> </bean> <!-- Book TablePanel --> <bean id="bookTablePanel" class="info.joseluismartin.gui.table.TablePanel" parent="tablePanel"> <property name="table" ref="bookTable" /> <property name="filterView" ref="filterView" /> <!-- bean name of model editor, show it on double-clicks on rows --> <property name="editorName" value="bookDialog" /> <property name="guiFactory" ref="guiFactory" /> <property name="persistentService" ref="bookService" /> </bean> <bean id="filterView" class="org.jdal.samples.library.ui.BookFilterView" parent="view"> <property name="categoryService" ref="categoryService" /> </bean>
Swing Namespace
A partir de la versión 1.2.1, jdal-swing incluye un espacio de nombres personalizado de spring que simplifica considerablemente la configuración del apartado anterior. Utilizando la etiqueta table, el modelo de la tabla, la tabla y el panel con el paginador pueden declararse en una única definición.
<swing:table entity="org.jdal.samples.library.model.Book" filterView="filterView" tableService="tableService"> <swing:columns> <swing:column name="name" displayName="Name" /> <swing:column name="author" displayName="Author" /> <swing:column name="category" displayName="Category" sortProperty="category.name" /> <swing:column name="isbn" displayName="ISBN" /> </swing:columns> </swing:table>
List Panel
Por último unimos todos los componentes visuales en un ListPane un componente similar a un JTabbedPane que utiliza una lista para mostrar las opciones. ListPane acepta únicamente PanelHolders como componentes, por lo que es necesario envolver las vistas y los componentes swing en el PanelHolder apropiado.
<bean id="listPanel" class="info.joseluismartin.gui.ListPane"> <property name="panels"> <list value-type="info.joseluismartin.gui.PanelHolder"> <bean class="info.joseluismartin.gui.JComponentPanelHolder"> <property name="name" value="Books" /> <property name="component" ref="bookTablePanel" /> </bean> <bean class="info.joseluismartin.gui.ViewPanelHolder"> <property name="view" ref="authorEditor" /> </bean> <bean class="info.joseluismartin.gui.ViewPanelHolder"> <property name="view" ref="categoryEditor" /> </bean> </list> </property> </bean>