Master J2EE de Oracle: Paso 7 de 12: Diseñar e Implementar Interfaces de Aplicación Web

Puede encontrar la versión original de este artículo en Inglés en:

http://www.oracle.com/technology/pub/articles/masterj2ee/index.html

Aprenda cómo crear interfaces de aplicación web funcionales y fáciles de utilizar

Descargas necesarias para este artículo

Consideraciones de Diseño

Hay muchas cosas que hacer en el orden correcto para conseguir un interface de aplicación Web funcional y fácil de utilizar:

  • Navegación:
    Como una aplicación web se ejecuta en el navegador, los usuarios esperan poder utilizar los botones de navegación del navegador, usar el botón Back para volver a la página anterior, por ejemplo. Sin embargo, manejar correctamente este tipo de navegación es delicado, por eso es importante diseñar un interface que asegure que el usuario utiliza otras cosas para la navegación. Recomiendo que diseñe su interface de usuario mirando lo más posible a los interfaces de usuarios tradicionales (GUI) - utilizando elementos cómunes de los GUIs, como árboles para seleccionar ítems, pestañas para mostrar los diferentes aspectos del ítem seleccionado, y enlaces del tipo barra de menú en la parte superior. Tenga en cuenta también que una aplicación Web está orientada a la tarea, lo que implica páginas que se deben completar en una secuencia específica, en vez de los enlaces libres de un site web tradicional. Sólo debe permitir a los usuario saltar directamente a páginas específicas, como las páginas principales para las diferentes tareas.
  • Bookmarks (Favoritos):
    Guardar en favoritos está muy relacionado con la navegación y también puede ser díficil de soportar en una aplicación Web; usted no querrá que un usuario guarde un enlace a una página a la que sólo se debería acceder como resultado del envío de un formulario, por ejemplo. Aunque no me gustan los frames HTML sobre una site web normal, pueden ser muy útiles en aplicaciones Web porque evitan que el usuario pueda guardar enlaces a páginas individuales.
  • Limitaciones de las Aplicaciones Web:
    Digamos la verdad: una aplicación web nunca podrá ser tan interactiva como una aplicación GUI tradicional, al menos no con las tecnologías de navegadores actuales. En un GUI, es fácil permitir que el usuario seleccione varias filas de una tabla y las borre todas con un sólo click. Por el contrario, en una aplicación Web, se debe manejar esto de forma diferente, por ejemplo situando un checkbox en cada fila que el usuario pueda pulsar para seleccionar esa fila. Un GUI tradicional también hace fácil activar o desactivar dinámicamente elementos de entrada basándose en la entrada del usuario, como cuando se pulsa un botón de radio o un checkbox. Implementar componentes de interface dinámicos en una aplicación Web normalmente implica la utilización de código JavaScript, pero si los usuarios tienen deshabilitado JavaScript en su navegador no podrá utilizarlo la aplicación. A menos que tenga completo control sobre la base de usuarios, debería proporcionar otras formas de presentar opciones a los usuarios, como la combinación de botones de radio con un botón para activar la nueva opción, o utilizar enlaces para las opciones.
  • Tamaño de la página:
    Aunque la gente está acostumbrada a moverse por la página para leer un articulo completo online, digamos, los usuarios encuentran las apliaciones Web más fáciles de utilizar cuando toda la información relacionada con cada tarea está en una sóla página, o si el desplazamiento está limitado a una sóla parte de la página (con la ayuda de frames internos), o utilizando botones Anterior/Siguiente para una tabla muy grande, por ejemplo.

Según mi experiencia, una buena forma de verificar lo más pronto posible que el interface de usuario tiene sentido es trabajar con maquetas y borradores del Manual de Usuario. Antes de escribir ni una línea de código, creo páginas HTML normales y las utilizo como 'pantallazos' en las primeras versiones del Manual de Usuario, y le pido a lo usuarios finales que lo revisen. Si nunca ha probado esta táctica anteriormente, se sorprenderá de lo bien que funciona para descubrir no sólo los problemas de diseño del interface, sino también los requerimientos no entendidos, funcionalidades olvidadas y otros muchos problemas.

Consideraciones de Implementación

Una vez que está satisfecho con el interface de usuario que ha diseñado, debe decidir cómo implementarlo. Con Java, tiene muchas opciones.

Si su aplicación requiere un interface muy interactivo, podría querer desarrollar una aplicación GUI en vez de una aplicación Web. Desplegar y mantener aplicaciones GUI totalmente personalizadas es casi tan fácil como para una aplicación Web, gracias a Java Web Start de Sun. Sin embargo, muchos usuarios de Internet todavía consideran Java Web Start una barrera de entrada demasiado elevada, por eso tienen su espacio las aplicaciones Web.

Hasta hace poco, la mayoría de aplicaciones Web se implementaban utilizando JavaServer Pages (JSP) o algún otro marco de trabajo comparable, como Apache Velocity. Estas tecnologías son ideales para sites web con contenido generado dinámicamente basado en entradas de usuario muy limitadas.

Sin embargo, para interfaces de usuario con interaciones complejas, las tecnologías de plantillas de páginas como éstas empiezan a mostrar sus limitaciones. Por ejemplo, incluso el sencillo acto de mostrar un conjunto de checkboxes con marcas de chequeo para representar las selecciones actuales requiere una enorme cantidad de lógica condicional en la propia plantilla de página, tanto si se implementa con scriptlets Java, acciones de JSP Standard Tag Library (JSTL) y el lenguaje de expresión (EL), o el lenguaje de plantillas de Velocity (VTL). A pesar de todo, en muchos casos JSP o las tecnologías JSP son la elección adecuada.

La Especificación JavaServer Faces (JSF), liberada en Marzo del 2004, es una mejor solución para los interfaces de aplicaciones web complejos. JSF define un modelo de desarrollo de aplicaciones Web basado en componentes, permitiendo a los vendedores y a los proyectos de código abierto crear sofisticados artefactos para interfaces de usuarios que los desarrolladores puedan utilizar para crear aplicaciones Web fáciles de utilizar, con portabilidad entre herramientas y servidores de aplicaciones. (Oracle es un activo contribuyente a la especificación JSF y continúa innovando para proporcionar productos basados en la especificación como las primeras versiones de la suite de componentes ADF. Además, Oracle se ha comprometido a dar soporte extensivo de JSF en una versión venidera de Oracle JDeveloper IDE).

Con el modelo de componentes de JSF, toda la lógica, como el código condicional necesario para chequear el estado de los checkboxes, se implementa en el componente, no en el código que hay dentro de la página. El modelo de eventos de JSF se asegura del desacoplamiento entre componentes y acciones, y le permite desarrollar interfaces de aplicaciones Web de forma similar a la que ahora desarrolla una aplicación GUI. La Suite de componentes ADF Faces y otras muchas suites tanto open suource como comerciales, le ofrecen la mayoría de los componentes que necesitará, pero si no encuentra el componente perfecto para su aplicación, puede implementar uno personalizado extendiendo clases JSF e implementando interfaces JSF.

Si decide utilizar JSF, debería tener cuidado con estos problemas que no son demasiado obvios:

  • Botones o enlaces de comando
    JSF proporciona dos componentes estándar para enviar un formulario (un botón de comando o un enlace de comando). El enlace de comando utiliza código JavaScript para enviar el formulario, por eso si no puede asegurarse de que todos sus usuarios tienen activado JavaScript, debería elegir los botones de comando en lugar de los enlaces.
  • Utilización de fragmentos de páginas
    En interfaces de usuario complejos, debería utilizar ficheros separados para partes individuales y pegarlos utilizando un fichero maestro. Esta aproximación hará que la aplicación sea más fácil de desarrollar y de mantener, y también permite reutilizar las mismas partes en varias páginas. Por ejemplo, si usted desarrolla aplicaciones JSF utilizando JSP, podrá crear el fichero maestro utilizando acciones dinámicas como <jsp:include> o <c:import>, o la directiva estática <%@ include %>, para unir todos los fragmentos. (Yo recomiendo utilizar las inclusiones estáticas siempre que sea posible para evitar los requerimientos acidicionales y los problemas asociados con las inclusiones dinámicas).
  • JSP o no
    Aunque JSP es la única tecnología para unir un interface de aplicación Web con componentes JSF descrito totalmente en la especificación, no es su única opción. El API JSF es lo suficientemente flexible como para permitirle utilizar otras tecnologías, como los ficheros XML. JSP es algo familiar para muchos desarrolladores de aplicaciones Web pero cuando se acostumbran a JSF, lanzan lejos todo lo que les estorba. (Para más información sobre esos problemas, puede ver el artículo Improving JSF by Dumping JSP)

Ahora que ya ha visto algunas de las cosas que necesita tener en consideración cuando implemente interfaces de aplicaciones Web, profundizaré en un aspecto de los interfaces de aplicaciones Web cuando se utilizan JavaServer Faces, especificamente, cómo personalizar los mensajes de error estándar. Veamos cómo las implementación de atributos genéricos y de un PhaseListener puede ayudarle a personalizar los mensajes de error generados por los conversores y validadores estándars de JSF.

Añadir Signficado a los Campos de Referencia

JSF define varios conversores y validadores que usted puede adjuntar a los componentes para validar la entrada del usuario. Estos crean una cola de mensajes de error cuando la entrada no es válida, y los componentes message luego muestran el mensaje al usuario. Por ejemplo, esta página JSP crea un componente de entrada que requiere un valor numérico entre 1 y 10, y dos componentes de mensajes para mostrar mensajes detallados y un sumario.

<%@ page contentType="text/html" %>
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>

<f:view>
    <h:messages layout="table" showDetail="true" showSummary="false" />
    <h:form>
        <h:panelGrid columns="3">
            <h:outputText value="Number of passengers:"/>
             <h:inputText id="noOfPass" size="8" required="true">
                <f:convertNumber integerOnly="true" />
                <f:validateLongRange minimum="1" maximum="10"/>
            </h:inputText>
            <h:message for="noOfPass" showDetail="false" showSummary="true" 
                style="color: red" />
        </h:panelGrid>
        <h:commandButton value="Submit"/>
    </h:form>
</f:view>
		

Los mensajes de errores de validación normalmente se muestran juntos en la parte superior de la página, pero esto sólo funciona si el usuario puede emparejar fácilmente cada mensaje con su correspondiente campo inválido. Los mensajes de error estándar de JSF sólo proporcionan una descripción del problema, sin ninguna información sobre el campo al que se refiere el mensaje, como en este mensaje devuelto por la Implementación de Referencia de JSF 1.0 de Sun cuando al validador del ejemplo anterior se le da un valor fuera de rango:

Validation error: Specified attribute is not between the expected values of 1 and 10
		

Para solucionar este problema, necesitamos dos cosas: una forma de asociar una referencia de un campo que sea amigable para el usuario y una forma de añadir esta referencia al mensaje de texto. Usemos un atributo de componente genérico para especificar el campo referencia para el componente:

<f:view>
    <h:messages layout="table" showDetail="true" showSummary="false" />
    <h:form>
        <h:panelGrid columns="3">
            <h:outputText value="Number of passengers:"/>
             <h:inputText id="noOfPass" size="8" required="true">
                <f:convertNumber integerOnly="true" />
                <f:validateLongRange minimum="1" maximum="10"/>
                <f:attribute name="fieldRef" value="Number of passengers" />
            </h:inputText>
            <h:message for="noOfPass" showDetail="false" showSummary="true" 
                style="color: red" />
        </h:panelGrid>
        <h:commandButton value="Submit"/>
    </h:form>
</f:view>
		

Un atributo genérico es un valor personalizado de un componente, que tienen un nombre y que otros trozos de código con acceso al código pueden utilizar. El elemento de acción <f:attribute> activa un atributo genérico llamado fieldRef con un valor que corresponde con la etiqueta del campo de entrada que ve el usuario.

Luego necesitamos algo que pueda obtener el atributo genérico e insertarlo en el mensaje. La mejor forma de hacer esta tarea es un PhaseListener. personalizado. Una aplicación puede registrar una o más implementaciones de PhaseListener:

<faces-config>
    <lifecycle>
        <phase-listener>
            com.mycompany.jsf.listeners.MessageListener1
        </phase-listener>
    </lifecycle>
</faces-config>
		

JSF llama a la clase PhaseListener antes y después de la fase de petición-procesamiento en la que está interesado. Esta implementación de PhaseListener maneja nuestras necesidades de personalización de mensajes:

package com.mycompany.jsf.listeners;

import java.util.Iterator;

import javax.faces.application.FacesMessage;
import javax.faces.component.UIComponent;
import javax.faces.component.UIViewRoot;
import javax.faces.context.FacesContext;
import javax.faces.event.PhaseEvent;
import javax.faces.event.PhaseId;
import javax.faces.event.PhaseListener;

public class MessageListener1 implements PhaseListener {

    public PhaseId getPhaseId() {
        return PhaseId.RENDER_RESPONSE;
    }

    public void beforePhase(PhaseEvent e) {
        FacesContext fc = e.getFacesContext();
        UIViewRoot root = fc.getViewRoot();
        Iterator i = fc.getClientIdsWithMessages();
        while (i.hasNext()) {
            String clientId = (String) i.next();
            UIComponent c = root.findComponent(clientId);
            String fieldRef = (String) c.getAttributes().get("fieldRef");
            if (fieldRef != null) {
                Iterator j = fc.getMessages(clientId);
                while (j.hasNext()) {
                    FacesMessage fm = (FacesMessage) j.next();
                    fm.setDetail(fieldRef + ": " + fm.getDetail());
                }
            }
        }
    }

    public void afterPhase(PhaseEvent e) {
    }
}
		

La clase MessageListener1 devuelve PhaseId.RENDER_RESPONSE desde el método getPhaseId(), por eso JSF llama a su método beforePhase() justo antes de redibujar la respuesta. Este método, primero obtiene las IDs de cliente de todos los componentes para los que se han puesto mensajes en la cola. Luego localiza dichos componentes con la ayuda del método findComponent() y obtiene el valor del atributo fieldRef asignado al componente en la página JSP. Finalmente, obtiene todos los mensajes del componente y añade al principio de cada mensaje la referencia del campo. Cuando JSF envía la respuesta al renderizador, el componente <h:messages> de la parte superior de la página dibuja los mensajes detallados modificados, incluyendo las referencias de los campos como se ve en la Figura 1:


Figura 1: Edición de una Página JSP en Oracle JDeveloper 10g

La Figura 1 muestra la edición de una página JSP en JDeveloper 10g, donde se ha lanzado un navegador pidiendo a JDeveloper que ejecute la página JSP. Por supuesto que puede usar un editor de texto normal para desarrollar aplicaciones JSF, pero JDeveloper lo hace un poquito más fácil, con la 'completación' de código para los elementos de la librería de etiquetas y otras bonitas características.

Sin embargo, tenga cuidado, ya que JDeveloper 10g no distingue entre una página JSP normal y una página JSP que contiene componentes JSF, por eso lanza el navegador con una URL que no corresponde con el patrón de URL para el servlet JSF. Usted debe cambiar manualmente la URL en el navegador para hacer que funcione; en otras palabras, debe cambiar la extensión ".jsp" por ".faces" si el servlet JSF está mapeado a "*.faces". (Puede encontrar más información en "How To Use JSF with JDeveloper 10g").

He sido un poco caprichoso en este ejemplo y también he situado un asterisco (*) rojo junto al campo inválido, usando un componente <h:message> combinado con un mensaje de sumario personalizado que contiene sólo el asterico:

...
<f:view>
    <h:messages layout="table" showDetail="true" showSummary="false" />
    <h:form>
        <h:panelGrid columns="3">
            <h:outputText value="Number of passengers:"/>
            <h:inputText id="noOfPass" size="8" required="true">
                ...
            </h:inputText>
            <h:message for="noOfPass" showDetail="false" showSummary="true" 
                style="color: red" />
        </h:panelGrid>
        <h:commandButton value="Submit"/>
    </h:form>
</f:view>
		

Un mensaje JSF tiene un texto de sumario y otro detallado, y puede personalizar ambas partes para todos los mensajes estándar sobreescribiendolos en un paquete de recursos declarado en el fichero faces-config.xml:

    <faces-config>
        <application>
            <message-bundle>custMessages</message-bundle>
        </application>
    </faces-config>
		

El fichero de paquete de recursos, custMessages.properties situado en el directorio WEB-INF/classes se parece a este, con un asterisco como texto para el mensaje de sumario de "not in range" y un texto personalizado para el mensaje detallado:

javax.faces.validator.NOT_IN_RANGE=*
javax.faces.validator.NOT_IN_RANGE_detail=Please enter a number between {0} and {1}.
		

Las claves de mensaje para todos los mensajes estándar están en el especificación JSF.

Si usted utiliza componentes Oracle ADF Faces en vez de los componentes estándar de JSF, no necesitará utilizar un componente <h:message> para añadir un asterico al campo inválido, ya que todos los componentes de entrada de ADF Faces automáticamente resaltan las entradas inválidas situando un icono de error en frente del los campos y un mensaje de error después de el campo. De cualquier forma, la técnica para añadir referencias amigables en el texto de los mensaje de error de esta forma puede ser múy útil, incluso para ADF Faces, porque al igual que los componentes estándar, a los mensajes de ADF Faces les falta una referencia amigable, aunque, por otro lado, el componente <af:messages> de ADF renderiza mensajes como enlaces a los campos a los que se refieren, para que el usuario pueda pulsar sobre el enlace e identificar un mensaje con su campo para evitar confusiones.

Añadir Nuevos Elementos Dinámicos a los Mensajes Estándar

Algunos mensajes estándar de JSF contienen elementos dinámicos, como el valor que está fuera de rango en un validador. Sin embargo, los mensajes de error estándar de los conversores no contienen ningún elemento dinámico:

    Conversion error occurred
		

Incluir el valor inválido en los mensajes de error de conversión los hará más fáciles de entender, por eso haremos un versión extendida del oyente, junto con algunos mensajes adicionales pesonalizados; empezaremos con los mensajes del fichero de paquete de recursos:

javax.faces.component.UIInput.CONVERSION=*
javax.faces.component.UIInput.CONVERSION_detail=CONV_ERR_MSG
CUST_CONV_ERR_MSG_detail=''{0}'' is not a valid format for this field
		

Estas entradas definen un texto detallado personalizado (CONV_ERR_MSG) para el mensaje de conversión estándar que el oyente puede reconocer, y una nueva rama de menssaje para el mensaje de error personalizado que contiene un espacio reservado para el valor inválido.

Las partes interesantes de la nueva versión del oyente se parecerán a esto:

...
public class MessageListener2 implements PhaseListener {
...
    public void beforePhase(PhaseEvent e) {
        FacesContext fc = e.getFacesContext();
        UIViewRoot root = fc.getViewRoot();
        String mbName = fc.getApplication().getMessageBundle();
        Locale locale = root.getLocale();
        ResourceBundle rb = ResourceBundle.getBundle(mbName, locale);

        Iterator i = fc.getClientIdsWithMessages();
        while (i.hasNext()) {
            String clientId = (String) i.next();
            UIComponent c = root.findComponent(clientId);
            String fieldRef = (String) c.getAttributes().get("fieldRef");
            if (fieldRef != null) {
                Iterator j = fc.getMessages(clientId);
                while (j.hasNext()) {
                    FacesMessage fm = (FacesMessage) j.next();
                    String detail = fm.getDetail();
                    if ("CONV_ERR_MSG".equals(detail)) {
                        String custMsgPattern = rb.getString("CUST_CONV_ERR_MSG_detail");
                        Object[] params = new Object[1];
                        params[0] = ((EditableValueHolder) c).getSubmittedValue();
                        String custMsg = MessageFormat.format(custMsgPattern,params);
                        fm.setDetail(custMsg);
                    }
                    fm.setDetail(fieldRef + ": " + fm.getDetail());
                }
            }
        }
    }
...
}
		

Lo nuevo en este oyente es que el oyente chequea los mensajes buscando el string que hemos definido como texto los mensajes estándar de conversión (CONV_ERR_MSG) y los reemplaza con el texto del mensaje de error personalizado, sacado del paquete de recursos de la aplicación y formateado usando la clase MessageFormat para reemplazar el espacio reservado con el valor inválido envíado.

La versión EA7 de ADF Faces no permite personalizar los mensajes de error de conversión, por eso usted no puede utilizar este truco con componentes de entrada de ADF Faces. Por otro lado, los mensajes de error de conversión de ADF Faces ya incluyen el valor inválido, por lo tanto no será necesaria esta personalización particular si utiliza ADF Faces.

Utilizar Mensajes Por-Componente

Aunque generalmente recomiendo utilizar mensajes genéricos que relleno dinámicamente con valores específicos del componente durante la ejecución, en algunos casos quiero usar mensajes distintos para cada componente. Puede realizar esto combinando las soluciones que hemos visto hasta ahora.

Por ejemplo, digamos que quiere especificar textos diferentes para los mensajes de error de "value required" para cada componente. Podemos utilizar un atributo generico para el texto del mensaje, como hicimos anteriormente para la referencia del campo:

    ...
<f:view>
    <h:messages layout="table" showDetail="true" showSummary="false" />
    <h:form>
        <h:panelGrid columns="3">
            <h:outputText value="Number of passengers:"/>
            <h:inputText id="noOfPass" size="8" required="true">
                <f:convertNumber integerOnly="true" />
                <f:validateLongRange minimum="1" maximum="10"/>
                <f:attribute name="fieldRef" value="Number of passengers" />
               <f:attribute name="custMsg" value="Please enter the number of passengers" />
            </h:inputText>
            ...
        </h:panelGrid>
        <h:commandButton value="Submit"/>
    </h:form>
</f:view>
		

Podemos utilizar un string reconocible (VALUE_REQ_MSG) como texto detallado para el mensaje de error estándar de "value required":

javax.faces.component.UIInput.REQUIRED=*
javax.faces.component.UIInput.REQUIRED_detail=VALUE_REQ_MSG
		

Finalmente añadimos otro trozo de lógica al oyente:

...
public class MessageListener3 implements PhaseListener {
...
    public void beforePhase(PhaseEvent e) {
        FacesContext fc = e.getFacesContext();
        UIViewRoot root = fc.getViewRoot();
        String mbName = fc.getApplication().getMessageBundle();
        Locale locale = root.getLocale();
        ResourceBundle rb = ResourceBundle.getBundle(mbName, locale);

        Iterator i = fc.getClientIdsWithMessages();
        while (i.hasNext()) {
            String clientId = (String) i.next();
            UIComponent c = root.findComponent(clientId);
            String fieldRef = (String) c.getAttributes().get("fieldRef");
            if (fieldRef != null) {
                Iterator j = fc.getMessages(clientId);
                while (j.hasNext()) {
                    FacesMessage fm = (FacesMessage) j.next();
                    String detail = fm.getDetail();
                    if ("CONV_ERR_MSG".equals(detail)) {
                        ...
                    }
                    else if ("VALUE_REQ_MSG".equals(detail)) {
                        String custMsg = (String) 
                        c.getAttributes().get("custMsg");
                        fm.setDetail(custMsg);
                    }
                    fm.setDetail(fieldRef + ": " + fm.getDetail());
                }
            }
        }
    }
    ...
}
		

Esta versión del oyente reemplaza el texto del mensaje con el valor del atributo genérico del componente custMsg cuando encuentra un mensaje "value required".

Al igual que con los mensajes de error de conversión, la versión AE7 de ADF Faces no le permite personalziar el mensaje de "value required", pero puede utilizar esta técnica para mensajes de error de validación incluso con componentes ADF Faces.

Sumario

Espero que algunos de los puntos de consideración que he revisado en este artículo le ayuden a diseñar e implementar mejores interfaces de aplicaciones Web. Hay un montón de libros, artículos y tutoriales online para aprender a utilizar JSF. Un buen lugar para empezar es http://java.sun.com/j2ee/javaserverfaces.

Los desarrolladores que utilizan JSF normalmente se preguntan cómo incluir etiquetas de los componentes que puedan ser leídas por humanos, como "Primer Apellido", en los mensajes de error generados por los valiadores y conversores estándar de JSF, se puede hacer utilizando atributos genéricos y un PhaseListener, como hemos visto en este artículo. Los tres métodos de personalización de mensajes descritos en este artículo deberían cubrir casi todas las necesidades, pero la idea básica presentada también se puede ampliar fácilmente para solucionar problemas similares que se pueda encontrar.

Próximos Pasos

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP