El componente Grid de Apache Tapestry

Escrito por el .
java planeta-codigo tapestry programacion
Enlace permanente Comentarios

En la mayoría de aplicaciones no solo es habitual sino algo muy usado el mostrar listados de elementos de forma tabular con paginación y columnas ordenables. En estos listados el complejo componente internamente Grid de Apache Tapestry pero a la vez muy sencillo de usar puede marcar una diferencia significativa en el número de líneas de código necesarias a escribir, la flexibilidad, funcionalidad ofrecida, la productividad al hacer la implementación o modificarla comparándolo con lo necesario en otros frameworks web en los que no hay nada comparable de serie.

Apache Tapstry

Apache Tapestry es uno de los muchos frameworks disponibles en Java para el desarrollo de aplicaciones y páginas web. A diferencia de la mayoría se basa en componentes y proporciona una larga lista de ellos listos para usar de serie, pero también se pueden crear componentes nuevos basados en los propios de Tapestry o los que desarrollemos nosotros muy fácilmente. Los componentes son piezas reusables de código que se pueden reutilizar bien directamente o para formar nuevos componentes y es uno de los motivos por los que en Tapestry se consigue una alta productividad además de otros beneficios como la encapsulación.

Uno de los componentes más complejos pero al mismo tiempo muy simple de usar ofrecidos por el framework es el componente Grid. El componente Grid muestra en una tabla un listado de datos ofreciendo las funcionalidades de paginación, ordenación, personalización de columnas, filtrado de columnas, personalización en caso de estar vacío y algunas cosas más. Lo único que debemos tener en cuenta para aprovechar al máximo el componente Grid son los parámetros que declara en su documentación su funcionamiento interno nos es irrelevante, será de los componentes más complejos y no por ello no es más difícil de utilizar basta decir que solo tiene un parámetro requerido y que es lo único imprescindible que es la lista de datos a mostrar.

Aunque el componente tiene un buen número de parámetros para personalizar según queramos su comportamiento basta que hagamos uso únicamente del parámetro source que es la fuente de datos del Grid, puede ser un objeto de tipo Collection o un GridDataSource que proporciona métodos para hacer la paginación y ordenación eficientemente recuperando de la base de datos o fuente de datos únicamente los registros a mostrar realizando paginación.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<t:block id="listaBlock">
	<h1>Lista de productos</h1>
	<t:grid source="source" row="producto" model="model" rowsPerPage="2" lean="true" inPlace="true" class="table table-bordered table-condensed">
		<p:nombreCell>
			<t:pagelink page="admin/producto" context="[producto.id, 'edicion']">${producto.nombre}</t:pagelink>
		</p:nombreCell>
		<p:actionCell>
			<t:eventlink event="eliminar" context="producto.id" class="btn btn-danger btn-xs" style="color: white;">Eliminar</t:eventlink>
		</p:actionCell>
		<p:empty>
			<p>No hay productos.</p>
		</p:empty>
	</t:grid>
</t:block>
ProductoAdmin.tml
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
package io.github.picodotdev.plugintapestry.pages.admin;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.tapestry5.Block;
import org.apache.tapestry5.ComponentResources;
import org.apache.tapestry5.SymbolConstants;
import org.apache.tapestry5.annotations.Cached;
import org.apache.tapestry5.annotations.Component;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.beaneditor.BeanModel;
import org.apache.tapestry5.corelib.components.Form;
import org.apache.tapestry5.grid.GridDataSource;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.ioc.annotations.Symbol;
import org.apache.tapestry5.services.BeanModelSource;
import org.apache.tapestry5.services.TranslatorSource;
import org.hibernate.Session;
import org.jooq.DSLContext;

import io.github.picodotdev.plugintapestry.entities.jooq.tables.pojos.Producto;
import io.github.picodotdev.plugintapestry.misc.JooqGridDataSource;
import io.github.picodotdev.plugintapestry.misc.Pagination;
import io.github.picodotdev.plugintapestry.services.dao.JooqProductoDAO;

/**
 * @tapestrydoc
 */
public class ProductoAdmin {

	private enum Modo {
		ALTA, EDICION, LISTA
	}

	@Inject
	private JooqProductoDAO dao;

    @Inject
    private Session session;
	
	@Inject
	private DSLContext context;

	@Inject
	@Symbol(SymbolConstants.TAPESTRY_VERSION)
	@Property
	private String tapestryVersion;

	@Inject
	private TranslatorSource translatorSource;

	@Inject
	private BeanModelSource beanModelSource;

	@Inject
	@Property
	private Block listaBlock, edicionBlock, botonesEdicionBlock;

	@Inject
	private ComponentResources resources;

	@Component
	private Form form;

	private Modo modo;

	@Property
	private Producto producto;

	void onActivate(Long id, Modo modo) {
		setModo((modo == null) ? Modo.LISTA : modo, (id == null) ? null : dao.findById(id));
	}

	Object[] onPassivate() {
		return new Object[] { (producto == null) ? null : producto.getId(), (modo == null) ? null : modo.toString().toLowerCase() };
	}

	void setupRender() {
		if (modo == null) {
			setModo(Modo.LISTA, null);
		}
	}

	void onPrepareForSubmitFromForm(Long id) {
		if (id != null) {
			// Si se envía un id se trata de una edición, buscarlo
			producto = dao.findById(id);
		}
		if (producto == null) {
			producto = new Producto();
		}
	}

	Object onCanceledFromForm() {
		setModo(Modo.LISTA, null);
		return ProductoAdmin.class;
	}

	void onSuccessFromForm() {
		dao.persist(producto);

		setModo(Modo.LISTA, null);
	}

	void onNuevo() {
		setModo(Modo.ALTA, null);
	}

	void onEliminarTodos() {
		dao.removeAll();
		setModo(Modo.LISTA, null);
	}

	void onEliminar(Long id) {
		producto = dao.findById(id);
		dao.remove(producto);

		setModo(Modo.LISTA, null);
	}

	public boolean hasProductos() {
		return getSource().getAvailableRows() > 0;
	}

	public GridDataSource getSource() {
		return new JooqGridDataSource(context, Producto.class) {
			@Override
			public int getAvailableRows() {
				return (int) dao.countAll();
			}
			
			@Override
			public List find(Pagination pagination) {
				return dao.findAll(pagination);
			}
		};
	}

	public BeanModel<Producto> getModel() {
		BeanModel<Producto> model = beanModelSource.createDisplayModel(Producto.class, resources.getMessages());
		model.exclude("id");
		model.add("action", null).label("").sortable(false);
		return model;
	}

	public Block getBlock() {
		switch (modo) {
			case ALTA:
			case EDICION:
				return edicionBlock;
			default:
			case LISTA:
				return listaBlock;
		}
	}
	
	// La anotacion @Cached permite cachar el resultado de un método de forma
	// que solo se evalúe
	// una vez independientemente del número de veces que se llame en la
	// plantilla de visualización.
	@Cached
	public Map<String, String> getLabels() {
		Map<String, String> m = new HashMap<String, String>();
		switch (modo) {
			case ALTA:
				m.put("titulo", "Alta producto");
				m.put("guardar", "Crear producto");
				break;
			case EDICION:
				m.put("titulo", "Modificación producto");
				m.put("guardar", "Modificar producto");
				break;
			default:
		}
		return m;
	}

	private void setModo(Modo modo, Producto producto) {
		switch (modo) {
			case ALTA:
				this.producto = new Producto();
				break;
			case EDICION:
				if (producto == null) {
					modo = Modo.ALTA;
					this.producto = new Producto();
				} else {
					this.producto = producto;
				}
				break;
			default:
			case LISTA:
				this.producto = null;
				break;

		}
		this.modo = modo;
	}
}
ProductoAdmin.java

Componente Grid de Tapestry

Componente Grid de Tapestry
  • source

Con los parámetros include y exclude podemos determinar que propiedades de los objetos o beans de la fuente de datos se incluyen en el Grid, con el parámetro add podemos añadir nuevas columnas y personalizarlas con los datos que necesitemos como sería el caso de añadir una columna con un Checkbox por fila para realizar una selección múltiple o de una columna con botones para realizar acciones. Para ambas cosas en el cuerpo del componente_Grid_definimos subcomponentes con la siguiente nomenclatura <p:[nombreColumna]Cell>, en en ejemplo usando <p:nombreCell> y <p:actionCell>. Las celdas de las columnas por defecto hacen un toString() de la propiedad del bean de la fila a mostrar, si queremos cambiar este comportamiento como en la columna nombre definimos la etiqueta <p:nombreCell> y dentro incluimos el contenido que deseemos que puede contener otros componentes en este caso el nombre con un enlace.

  • include
  • exclude
  • add
  • <p:[nombreColumna]Cell>

Con el parámetro rowsPerPage podemos cambiar el número de filas por página del Grid, en el ejemplo son 2 pero puede ser la cifra que deseemos y tampoco tiene por que ser una constante, el número de filas a mostrar puede provenir de una expresión y cambiar según alguna lógica. Los parámetros columnIndex, rowIndex y row nos proporcionan información del índice de la columna actual, índice de la fila actual y el objeto actual de la fila respectivamente que podemos usar al personalizar las celdas del Grid. Son parámetros de salida que el Grid se encarga de proporcionarnos según procesa las filas y celdas, en base a ellos podremos implementar alguna funcionalidad.

  • rowsPerPage
  • columnIndex
  • rowIndex
  • row

Los parámetros informales (denominados así para aquellos que le pasamos al Grid que no están declarados explícitamente en su interfaz o contrato y que no proporcionan alguna funcionalidad) son incluidos en la etiqueta table del HTML que genera el Grid. Igualmente el parámetro informal class se incluye tal cual se indica en el atributo en class de la tabla para personalizar los estilos y usando el parámetro rowClass se incluye en cada fila en su etiqueta tr de HTML. Además de estas clases que podemos el componente añade algunas clases más a ciertas filas: t-first para la primera fila, t-last para la última, t-sort-column-ascending y t-sort-column-descending para las columnas que estén ordenadas ascendentemente y descendentemente de forma que con CSS tengamos la posibilidad de cambiar sus estilos.

  • class
  • rowClass

Con el parámetro empty definimos un componente Block que se usará cuando el Grid no tenga filas que mostrar, lo que es útil para mostrar un mensaje indicando que la tabla no tiene filas como cuando no hay elementos.

Mensaje de un Grid sin elementos

Mensaje de un Grid sin elementos
  • empty

Con pagerPosition indicaremos si queremos la barra de paginación situada encima de la tabla, abajo, en ambas posiciones o no queremos.

  • pagerPosition

Por si fuera poco con el parámetro inPlace podemos hacer que la paginación y ordenación funcione usando AJAX de modo que no se recargue toda la página en cada pulsación de un enlace. No será necesario que añadamos nada de JavaScript, el componente se encargará de hacer la petición AJAX y con el resultado que sea devuelto actualizar la tabla.

  • inPlace

Finalmente, comentaré el parámetro encoder con el que podemos hacer que el componente Grid funcione cuando se usa dentro de un componente Form. La clase ValueEncoder transforma un objeto a un String que lo identifique en el cliente y a partir del identificador del cliente los transforme al objeto cuando se envíe de nuevo al servidor. Podemos indicar el ValueEncoder en cada Grid o definirlo como una configuración del contenedor IoC.

Hay algún parámetro más como sortModel y paginationModel para mantener la información de ordenación y paginación pero los anteriores son los que más habitualmente usaremos y probablemente con source, include, exclude y add tengamos suficiente para muchos casos.

Es sorprendentemente lo sencillo que es usar el componente Grid para toda la funcionalidad que proporciona. En ciertos casos este componente por si solo puede reducir drásticamente la cantidad de código necesario a escribir en las plantillas que producen HTML y aumentar notablemente la productividad al crear o modificar páginas con listados de elementos. En el artículo Mantenimiento CRUD en Apache Tapestry comento como conseguir un CRUD completo usando el componente Grid entre otras cosas.

Portada libro: PlugIn Tapestry

Libro PlugIn Tapestry

Si te interesa Apache Tapestry descarga gratis el libro de más de 300 páginas que he escrito sobre este framework en el formato que prefieras, PlugIn Tapestry: Desarrollo de aplicaciones y páginas web con Apache Tapestry, y el código de ejemplo asociado. En el libro comento detalladamente muchos aspectos que son necesarios en una aplicación web como persistencia, pruebas unitarias y de integración, inicio rápido, seguridad, formularios, internacionalización (i18n) y localización (l10n), AJAX, ... y como abordarlos usando Apache Tapestry.



Comparte el artículo: