Selección múltiple de filas en un datatable con JSF: haciéndolo nosotros mismos.
0. Índice de contenidos.
1. Introducción
Una vez analizadas la posibilidades que tenemos para
añadir selección
múltiple a nuestros listados en JSF, puede que los componentes que usamos no se adecúen totalmente a las necesidades de nuestros usuarios o
puede que no estemos usando ninguna librería de terceros, que proporcione dicho soporte. Tanto en un caso como en el otro, la solución es añadir nosotros
mismos dicha funcionalidad a nuestros dataTables.
En este tutorial vamos a examinar dicha posibilidad añadiendo, a un dataTable del estándar, una columna de selección múltiple que permita la selección
de una, varias o todas las filas del listado, esto es, facturas para marcar como pagadas, a través de una casilla de verificación, esto es, un checkBox.
Como véis, la historia de usuario que intentamos resolver es la misma que en el tutorial anterior:
selección múltiple de filas en un datatable con JSF: haciendo uso de librerías de componentes.
2. Entorno.
El tutorial está escrito usando el siguiente entorno:
- Hardware: Portátil MacBook Pro 17′ (2.93 GHz Intel Core 2 Duo, 4GB DDR3 SDRAM).
- Sistema Operativo: Mac OS X Snow Leopard 10.6.1
- JSF2 (Mojarra 2.0.4)
- Apache Tomcat 7.0.6
3. Nuestra selección múltiple.
Lo primero es el fuente de nuestro árbol de componentes JSF:
<h:form id="pForm"> <h:panelGroup> <h:dataTable id="invoicesList" var="invoice" value="#{invoiceView.invoices}" rows="10" binding="#{invoiceView.invoicesDataTable}"> <h:column> <f:facet name="header"> <h:selectBooleanCheckbox id="selectAllEntities" value="#{invoiceView.selectAllInvoices}" immediate="true" valueChangeListener="#{invoiceView.selectAllInvoicesListener}"> <f:ajax event="click" render="@form"/> </h:selectBooleanCheckbox> </f:facet> <h:selectBooleanCheckbox value="#{invoiceView.invoicesSelected[invoice]}" immediate="true" valueChangeListener="#{invoiceView.selectInvoiceListener}"> <f:ajax event="click" render="@form"/> </h:selectBooleanCheckbox> </h:column> <h:column> <f:facet name="header"> <h:outputText id="number_label" value="#{msg['Invoice.number']}"/> </f:facet> <h:outputText id="number" value="#{invoice.number}"/> </h:column> <h:column> <f:facet name="header"> <h:outputText id="client_label" value="#{msg['Invoice.client']}"/> </f:facet> <h:outputText id="label" value="#{invoice.client}"/> </h:column> <h:column> <f:facet name="header"> <h:outputText id="amount_label" value="#{msg['Invoice.amount']}"/> </f:facet> <h:outputText id="amount" value="#{invoice.amount}"/> </h:column> <h:column> <f:facet name="header"> <h:outputText id="invoiced_label" value="#{msg['Invoice.invoiced']}"/> </f:facet> <h:outputText id="invoiced" value="#{invoice.invoiced}"/> </h:column> </h:dataTable> <h:panelGroup> <h:commandButton actionListener="#{invoiceView.markAsInvoicedListener}" value="#{msg['components.datatableMultipleSelection.bill']}"/> </h:panelGroup> </h:panelGroup> <h:panelGroup id="selectedInvoicesPanel"> <h:outputText value="#{msg['components.datatableMultipleSelection.selectedInvoices']}" /> <ul> <ui:repeat var="invoice" value="#{invoiceView.allInvoicesSelected}" > <li><h:outputText value="#{invoice.number} - #{invoice.client} - #{invoice.amount}" /></li> </ui:repeat> </ul> </h:panelGroup> </h:form>
Lo interesante se encuentra en el primer h:column en el que incluimos dos checkBoxes, uno en la cabecera y otro en cada fila (en cada item del listado) de modo que
produzcan dos eventos de cambio de valor:
- al pulsar sobre el de la cabecera se seleccionarán o deseleccionarán todas las facturas,
- pulsando sobre el checkBox de cada una de las filas se hará lo propio con la factura seleccionada
Los eventos se producen a través de Ajax de modo que en la respuesta se rerenderiza todo el formulario (@form), a nosotros nos interesa que se rerenderize el listado de la
parte inferior en el que mostramos las facturas seleccionadas.
Hay un tercer evento en la vista, correspondinete al botón de «facturar» que marcará como facturadas todas las filas seleccionadas.
El soporte del lado del managedBean es el siguiente:
package com.autentia.jsf.showcase.view; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import javax.annotation.PostConstruct; import javax.faces.bean.ManagedBean; import javax.faces.bean.ViewScoped; import javax.faces.component.UIData; import javax.faces.event.ActionEvent; import javax.faces.event.ValueChangeEvent; import org.apache.log4j.Logger; import com.autentia.jsf.showcase.core.AddSampleData; import com.autentia.jsf.showcase.core.Invoice; import com.autentia.jsf.showcase.core.FinancialService; @ManagedBean @ViewScoped public class InvoiceView { private static final long serialVersionUID = 5587527957231937424L; private static final Logger log = Logger.getLogger(InvoiceView.class.getName()); private List<Invoice> invoices; private boolean selectAllInvoices; private UIData invoicesDataTable; @PostConstruct protected void init(){ log.debug("init..."); AddSampleData.initializeInvoices(); } public List<Invoice> getInvoices(){ if (invoices == null){ invoices = FinancialService.getInstance().getAll(); } log.debug("allInvoices: " + invoices); return invoices; } public void setInvoicesDataTable(UIData invoicesDataTable) { this.invoicesDataTable = invoicesDataTable; } public UIData getInvoicesDataTable() { return invoicesDataTable; } public void markAsInvoicedListener(ActionEvent event){ final List<Invoice> invoicesSelected = getAllInvoicesSelected(); log.debug("markAsInvoicedListener " + invoicesSelected); FinancialService.getInstance().markAsInvoiced(invoicesSelected); } public void setSelectAllInvoices(boolean selectAllInvoices) { this.selectAllInvoices = selectAllInvoices; } public boolean isSelectAllInvoices() { return selectAllInvoices; } private Map<Invoice,Boolean> invoicesSelected = new HashMap<Invoice, Boolean>(){ private static final long serialVersionUID = -3360838896781243282L; @Override public Boolean get(Object object) { if (isSelectAllInvoices()){ invoicesSelected.put( (Invoice) object, Boolean.TRUE); return Boolean.TRUE; } if (!containsKey(object)){ return Boolean.FALSE; } return super.get(object); }; }; public Map<Invoice,Boolean> getInvoicesSelected() { return invoicesSelected; } public List<Invoice> getAllInvoicesSelected(){ if (isSelectAllInvoices()){ return getInvoices(); } final List<Invoice> result = new ArrayList<Invoice>(); final Iterator<?> iterator = invoicesSelected.keySet().iterator(); while (iterator.hasNext()) { Invoice key = (Invoice) iterator.next(); if (invoicesSelected.get(key)){ result.add(key); } } log.debug("allInvoicesSelected " + result); return result; } public void clearEntitiesSelected(){ selectAllInvoices = false; invoicesSelected.clear(); } public void selectAllInvoicesListener(ValueChangeEvent event){ selectAllInvoices = ((Boolean) event.getNewValue()).booleanValue(); if (!selectAllInvoices){ clearEntitiesSelected(); } } public void selectInvoiceListener(ValueChangeEvent event){ selectAllInvoices = false; invoicesSelected.put((Invoice)invoicesDataTable.getRowData(), (Boolean) event.getNewValue()); } }
Los aspectos destacados a comentar son los siguientes:
- hacemos uso de una referencia al dataTable a través del binding al objeto UIData invoicesDataTable, para obtener la fila seleccionada en el dataTable en los
eventos de cambio de valor, ya hemos visto que tenemos varias formas de
acceder a la fila seleccionada de un dataTable
nosotros hemos optado aquí por esta, - invoicesSelected mantiene un mapa de facturas que indica las que están seleccionadas y las que no. Dicho mapa no se inicializa, de ahí la necesidad de sobreescribir
el método get del mismo, para evitar problemas
de gestión en el evento de cambio de valor, en caso contrario se producirían N eventos de cambio de valor de null, que es lo que devolvería el método get de la
clase HashMap, a false. En dicho método se asigna el valor de seleccionado a las filas si se ha fijado la marca de «seleccionar todos» a través del evento correspondiente. - ¿por qué un mapa?, porque con el soporte de EL del que disponemos no podemos invocar a un método del controlador pasándole un parámetro, con lo que la única forma
de obtener si se ha seleccionado o no una fila es obtener el valor de un mapa, value=»#{invoiceView.invoicesSelected[invoice]}». Hay una segunda opción, que es
mantenter un atributo selected a nivel de entidad, pero lo desaconsejamos porque si trabajamos con entidades persistibles estamos mezclando lógica de presentación
con lógica de persistencia y su existencia exige recorrer el listado de entidades más de lo debido con lo que incide en rendimiento.
El resultado a nivel de interfaz visual es el siguiente:
Aplicándole los estilos corporativos de nuestro cliente, tendríamos las necesidades del mismo cubiertas.
4. Referencias.
5. Conclusiones.
Lo ideal es que esta funcionalidad estuviera encapsulada en un componente de la vista que no supusiera implicaciones en el controlador, para ello tenemos dos
opciones:
- crear o extender un componente nativo,
- incluirlo en un componente más ligero, basado en componentes por composición de facelets, como lo que tenemos nosotros en
wuija.
Recordad que si estáis interesados en el contenido de nuestros tutoriales y tenéis una necesidad formativa al respecto podéis poneros en contacto con nosotros. En Autentia
nos dedicamos, además de a la consultoría, desarrollo y soporte a desarrollo, a impartir cursos de formación de las tecnologías con las que trabajamos.
Un saludo.
Jose
Hola, excelente tutorial, me podrías enviar el war de tu proyecto con las librerías, ya que estoy bien interesando en aprender. GRACIAS por el apoyo.
mi correo es hack10.5@hotmail.com
Lo estaré esperando con ganas.
Espero que lo hayas recibido, por favor es urgente, es gusto lo que necesito para mi proyecto.
mi correo es hack10.5@hotmail.com
http://www.youtube.com/watch?v=s8MGMWzQEic
Yo tengo una pregunta. Tengo un proyecto en donde tengo mi View y mi XHTML pero al momento de seleccionar el checkbox y «obtener los seleccionados» me marca que no he seleccionado ninguno, a que se debe, le puse el RowKey, selectioon (id) y el value(lista de datos).