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:

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.

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()

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:

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>