Gestión del evento de cambio de valor en JSF2: valueChangeListener.

5
41547

Gestión del evento de cambio de valor en JSF2: valueChangeListener.

0. Índice de contenidos.


1. Introducción

En este tutorial vamos a exponer la mejor manera de gestionar un evento de cambio de valor en la vista con JSF2.

Para ello haremos uso de un formulario de entrada de datos en el que los posibles valores de un componente dependen del valor que introduzca el usuario en otro. Se trata
del típico ejemplo del selector de países que recarga el contenido del selector de ciudades.

Es requisito que la recarga del componente se produzca a través de Ajax, así iremos viendo que los cambios producidos en la vista repercuten en el managedBean puesto que
tendremos un área en el que visualizan los mismos datos de entrada como datos de salida.

En el formulario habrá algún componente adicional, marcado como obligatorio para comprobar que:

  • la petición que se lanza vía ajax no pierde los valores de los componentes visuales que se han rellenado hasta ese momento, y que
  • cuando se realiza la selección del componente no salta el mensaje de obligatoriedad de otros campos, ese solo se produce al pulsar el botón de guardar.


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)
  • Tomcat 7.0.6


3. El ejemplo práctico.

Primero vamos a exponer la vista, el código fuente del árbol de componentes jsf. Estamos en un formulario de edición de un cliente, en el que tenemos una serie de campos:

  • el nombre del cliente, que es obligatorio,
  • una marca que indica si el cliente es vip,
  • un selector de países, que al modificar el valor asignado provoca una recarga del siguiente selector,
  • un selector de ciudades, cuyos posibles valores dependen del valor elegido en el selector de países.
  <h:form id="pForm">
    <h:panelGroup>
      <h:panelGrid columns="3">
					
        <h:outputLabel for="name" value="#{msg['Client.name']}"/>
        <h:inputText id="name" value="#{clientView.client.name}" required="true">
          <f:ajax render="clientOnServerSide" execute="@this" />
        </h:inputText>
        <h:message for="name" />
	
        <h:outputLabel for="vip" value="#{msg['Client.vip']}"/>
        <h:selectBooleanCheckbox id="vip" value="#{clientView.client.vip}" valueChangeListener="#{clientView.vipChangeListener}"
          immediate="true" >
          <f:ajax render="clientOnServerSide" execute="@this" />
        </h:selectBooleanCheckbox>
        <h:message for="vip" />
	
        <h:outputLabel for="country" value="#{msg['Client.country']}"/>			
        <h:selectOneMenu id="country" value="#{clientView.country}" converter="selectItemsConverter" 
          valueChangeListener="#{clientView.countryChangeListener}" immediate="true">
          <f:ajax render="clientOnServerSide city" />
          <f:selectItem itemLabel="#{msg['noSelectOption']}" noSelectionOption="true"/>
          <f:selectItems value="#{clientView.countries}" var="country" itemValue="#{country}" itemLabel="#{country.name}" />
        </h:selectOneMenu>
        <h:message for="country" />
	
        <h:outputLabel for="city" value="#{msg['Client.city']}"/>			
        <h:selectOneMenu id="city" value="#{clientView.client.city}" converter="selectItemsConverter">
          <f:ajax render="clientOnServerSide" execute="@this" />
          <f:selectItems value="#{clientView.cities}" var="city" itemValue="#{city}" itemLabel="#{city.name}" />
        </h:selectOneMenu>
        <h:message for="city" />
													                
      </h:panelGrid>

    </h:panelGroup>

    <br />
    <h:commandButton action="#{clientView.save}" value="#{msg['btn.save']}" />
    <h:messages />
    <hr />
				
    <h:panelGroup id="clientOnServerSide">
      <h:panelGrid columns="2">
					
        <h:outputText value="#{msg['Client.name']}" />
        <h:outputText value="#{clientView.client.name}" />
						
        <h:outputText value="#{msg['Client.vip']}" />
        <h:outputText value="#{clientView.client.vip}" />
						
        <h:outputText value="#{msg['Client.country']}" />
        <h:outputText value="#{clientView.country.name}" />
						
        <h:outputText value="#{msg['Client.city']}" />
        <h:outputText value="#{clientView.client.city.name}" />

      </h:panelGrid>
    </h:panelGroup>
				
  </h:form>

Vamos a analizar los puntos más interesantes:

  • las líneas 5 a 9 contienen la etiqueta, el campo de entrada y los posibles mensajes relacionados con el nombre del cliente. Está marcado como obligatorio mediante
    el atributo required y tiene un evento ajax que al submitirse envía únicamente el componente de entrada y rerenderiza un panel inferior con los datos asignados
    al cliente en el managedbean.
  • las líneas 11 a 16 contienen la etiqueta, la casilla de verificación y los posibles mensajes relacionados con la marca de «very important people», el check tiene un evento
    de cambio de valor que invoca a un escuchador del lado del managedBean y está marcado como immediate para que al producirse el evento e invoque al managedBean
    no provoque la validación del resto de campos que no están marcados con immediate.
  • las líneas 18 a 23 contienen la etiqueta, el selector y los posibles mensajes relacionados con el país, el selector tiene un evento
    de cambio de valor que invoca a un escuchador del lado del managedBean y, al igual que el anterior, está marcado como immediate. Además:

    • el evento ajax que lanza rerenderiza el área inferior con los campos de salida y, además, el componente siguiente que permite seleccionar la ciudad,
    • tiene un conversor que permite convertir de bean a selecItem y viceversa, para más detalles consultar el tutorial
      JSF2 return,
    • hace uso de la etiqueta f:SelectItem para establecer un valor por defecto vació de «Seleccione…»
    • hace uso de la etiqueta f:SelectItems para establecer los posibles países recuperados de un método del managedBean.
  • las líneas 27 a 32 contienen la etiqueta, el selector y los posibles mensajes relacionados con la ciudad, cuyos posibles valores depende
    del valor del campo anterior.
  • las líneas 43 y siguientes contienen los campos de entrada convertidos en campos de salida.

El valor de todos los componentes del formulario, menos el país, se encuentran asociados a los correspondientes atributos de la entidad client del managedBean.
El país no se almacena en el cliente, puesto que al almacenarse la ciudad, el país se deduce y evitamos redundancias.

El soporte necesario a la vista del lado del managedBean es el siguiente:

package com.autentia.jsf.showcase.view;

import java.io.Serializable;
import java.util.List;

import javax.annotation.PostConstruct;
import javax.faces.application.FacesMessage;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.ViewScoped;
import javax.faces.context.FacesContext;
import javax.faces.event.ValueChangeEvent;

import org.apache.log4j.Logger;

import com.autentia.jsf.showcase.core.AddSampleData;
import com.autentia.jsf.showcase.core.HumanResourcesService;
import com.autentia.jsf.showcase.core.MetadataService;
import com.autentia.jsf.showcase.core.entities.Client;
import com.autentia.jsf.showcase.core.metadata.City;
import com.autentia.jsf.showcase.core.metadata.Country;

@ManagedBean
@ViewScoped
public class ClientView implements Serializable{

	private static final long serialVersionUID = -2377612760546575078L;

	private static final Logger log = Logger.getLogger(ClientView.class.getName());

	private Client client;

	private Country country;
	
	@PostConstruct
	protected void init(){
		log.debug("init...");
		AddSampleData.initializeMetadata();
		client = new Client("");
	}
	
	public void setClient(Client client) {
		log.debug("setClient:" + client);
		this.client = client;
	}

	public Client getClient() {
		log.debug("getClient:" + client);
		return client;
	}
	
	public void setCountry(Country country) {
		log.debug("setCountry:" + country);
		this.country = country;
	}

	public Country getCountry() {
		log.debug("getCountry:" + country);
		return country;
	}

	public void save(){
		log.debug("saving client:" + client);
		try{
			HumanResourcesService.getInstance().addClient(client);
			FacesContext.getCurrentInstance().addMessage("", new FacesMessage("Información almacenada correctamente."));
		}
		catch(Exception e){
			log.error("Exception saving the client",e);
			FacesContext.getCurrentInstance().addMessage("", new FacesMessage(e.getMessage()));
		}
	}

	public List<Country> getCountries(){
		log.debug("getCountries");
		return MetadataService.getInstance().getAllCountries();
	}
	
	public List<City> getCities(){
		log.debug("getCities");
		return MetadataService.getInstance().getCityByCountry(country);
	}

	public void vipChangeListener(ValueChangeEvent event){
		log.debug("vipChangeListener");
		// do nothing
	}

	public void countryChangeListener(ValueChangeEvent event){
		log.debug("countryChangeListener");
		country = ( (Country) event.getNewValue());
		City city = null;
		if (!getCities().isEmpty()){
			city = getCities().get(0);
		}
		client.setCity(city);
		FacesContext.getCurrentInstance().renderResponse();
	}

}

Nos vamos a centrar en los eventos que capturan los cambios de valor:

  • el método vipChangeListener escucha el evento de cambio de valor del selector que marca al cliente como vip y no necesita tener ninguna lógica de control
    puesto que, de dicho componente no depende ningún otro y porque la lógica de rerenderización ya ha sido configurada en el componente f:ajax anidado en la vista,
  • el método countryChangeListener sí tiene lógica de control porque queremos que al seleccionar un país se seleccione una ciudad por defecto, si no
    tuviéramos esa necesidad la lógica de control de este método también podría desaparecer. No siendo así, en este método:

    • se asigna a la variable country el valor del
      selector representado por el newValue,
    • se obtiene la primera ciudad del país, si la tuviera, para asignarla nivel de cliente.
    • se escapa del resto de fases del ciclo de vida de jsf (se ejecutará en la fase de APPLY_REQUEST_VALUES) para que no salten las validaciones ni se asignen
      los valores del resto de componentes al modelo.

El restulado a nivel de interaz de usuario sería el siguiente:



4. Identificación de posibles problemas.

Qué pasaría…

  • si los componentes que tienen los eventos de cambio de valor no estuvieran marcados como immediate, no se llegaría nunca a la fase de UPDATE_MODEL_VALUES
    porque saltarían las validaciones del resto de campos marcados como obligatorios (o con algún error de validación) y no marcados como immediate.
  • si el selector de países no tiene un valor por defecto y no inicializamos el valor del atributo correspondiente en el controlador, cuando se produce la
    primera submisión del formulario al managedBean se ejecuta el listener de cambio de valor, como éste tiene un renderResponse() vuelve a la vista sin producirse
    el evento que provocó la submisión. Si ponemos el ejemplo del botón de guardar, al pulsar no guardaría, solo asigna el
    valor del selector de ciudades, ya que se produce un evento de cambio de valor del null al primer valor del selector. El efecto es que el botón de guardar
    solo funciona si le pulsas dos veces (la segunda ya tiene asignado un valor).
  • si no realizamos un renderResponse(), en función de qué componentes estén viajando al servidor en la petición ajax, puede que la lógica de control que modifica
    el valor de otro componente no tenga efecto porque se vea sobrescrito por el valor del componente que viaja en la request,
  • si, por mis requisitos, las peticiones no pueden ser ajax, bastaría con eliminar los f:ajax e incluir en los componentes que tienen un evento de cambio de valor
    el atributo onchange=»submit();».


5. Conclusiones.

Es batante más sencillo e incurrimos en un menor número de posibles errores trabajando con eventos de cambio de valor en JSF2 que en JSF1.2, en gran parte por el mayor
nivel de granularidad que nos proporciona la rerenderización de componentes del atributo f:ajax, que se parece bastante a la que nos proporcionaba, hasta ahora, RichFaces
a través de ajax4JSF.

Para aquellos que piensen que la gestión de ajax en JSF2 implica tener un mayor nivel de control en la vista sobre qué se repinta y que se envía en cada evento de cada
componente, efectivamente y sí, pero yo lo prefiero y próximamente publicaremos un tutorial al respecto.

Un saludo.

Jose

jmsanchez@autentia.com

5 COMENTARIOS

  1. Estor tratando de implementar este ejemplo pero cuando hace el evento de valuechangeevent me genera el siguiente error:
    Caused by: java.lang.ClassCastException: java.lang.Boolean cannot be cast to java.lang.String
    estoy utilizando glassfish,jsf2, hibernate , derby, no se que estare haciendo mal, es necesario utlizar el converter. o a que se debe ese error.

  2. Buenas tardes

    Serias tan amable de colaborarme con esta inquietud. tengo el siguiente codigo y necesito que sea de caracter obligatodio para el usuario contestar con cualquier opcion, o ambas; pero en el componente (p:selectBooleanCheckbox) no funciona el required=»true».

    Muchas gracias por su colaboración.

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad