ViewBaseAction tiene una
propiedad
view que puedes usar desde dentro de
execute()
mediante
getView(). Este objeto del tipo
org.openxava.view.View
permite manejar la interfaz de usuario, en este caso borramos los datos
visualizados usando
getView().clear().
También usamos
addMessage(). Todos los mensajes añadidos con
addMessage()
se mostrarán al usuario al final de la ejecución de la acción. Puedes,
bien añadir el mensaje a mostrar, o bien un id de una entrada en
i18n/MensajesFacturacion_es.properties.
La siguiente imagen muestra el comportamiento del módulo
Factura
después de añadir la acción de borrar personalizada:

Por supuesto, este es un comportamiento tonto. Añadamos el comportamiento
real. Para marcar como borrada la factura actual sin borrarla realmente,
necesitamos añadir una nueva propiedad a
Factura. Llamémosla
eliminado:
@Hidden // No se mostrará por defecto en las vistas y los tabs
@Column(columnDefinition="BOOLEAN DEFAULT FALSE") // Para llenar con falses en lugar de con nulos
boolean eliminado;
Como ves, es una propiedad booleana simple y llana. El único
detalle es el uso de la anotación
@Hidden. Indica que cuando una
vista o lista tabular por defecto sea generada la propiedad
eliminado
no se mostrará; aunque si la pones explícitamente en
@View(members=)
o
@Tab(properties=) sí que se mostrará. Usa esta anotación para
marcar aquellas propiedades de uso interno del programador pero que no
tienen sentido para el usuario final.
Usamos
@Column(columnDefinition=) para llenar la columna con
falses
en lugar de con nulos. Aquí puedes poner la definición SQL de la columna
para enviar a la base de datos. Es más sencillo que actualizar la base de
datos pero el código es más dependiente de la base de datos.
Ya estamos preparados para escribir el código real de la acción:
public void execute() throws Exception {
Factura factura = XPersistence.getManager().find(
Factura.class,
getView().getValue("oid")); // Leemos el id de la vista
factura.setEliminado(true); // Modificamos el estado de la entidad
addMessage("object_deleted", "Factura"); // El mensaje de confirmación de borrado
getView().clear(); // Borramos la vista
}
El efecto visual es el mismo, se ve un mensaje y la vista se
borra, pero en este caso hacemos algo de lógica. Buscamos la entidad
Factura
asociada con la vista actual y entonces cambiamos el valor de su propiedad
eliminado. No necesitas hacer nada más, porque OpenXava confirma
automáticamente la transacción JPA. Es decir, puedes leer cualquier objeto
y modificar su estado en una acción, y cuando la acción finalice los
cambios se almacenarán en la base de datos.
Pero hemos dejado algunos cabos sueltos. El botón de "borrar" sigue en la
vista después de haber borrado la entidad, es decir, cuando no hay un
objeto seleccionado, además si el usuario lo pulsa la instrucción para
buscar fallará y un mensaje un tanto técnico e ininteligible se le
mostrará a nuestro desamparado usuario. Podemos refinar este caso no
mostrando el botón, tal como cuando pulsamos el botón
Nuevo.
Observa la ligera modificación al método
execute():
public void execute() throws Exception {
// ...
getView().clear();
getView().setKeyEditable(true); // Crearemos una nueva entidad
}
Con
getView().setKeyEditable(true) indicamos que creamos
una nueva entidad y como nuestra acción
delete tiene el atributo
disponible-en-nuevo="false", entonces, el botón de borrar no se
mostrará.
Ahora que ya sabes como escribir tus propias acciones personalizadas, es
tiempo de aprender como escribir código genérico.
Acciones genéricas
El código actual de
EliminarFactura refleja la forma típica de
escribir acciones. Es código concreto que accede directamente a entidades
concretas para manipularlas.
Pero a veces puedes encontrarte alguna lógica en tu acción susceptible de
ser usada y reusada por toda tu aplicación, incluso en todas tus
aplicaciones. En este caso, puedes utilizar algunas técnicas para crear
código más reutilizable y así convertir tus acciones personalizadas en
acciones genéricas.
Aprendamos estas técnicas para escribir código más genérico en nuestras
acciones.
Código
genérico con MapFacade
Imagínate que quieres usar tu
EliminarFactura también para
pedidos. Es más, imagínate que quieres usarla para cualquier entidad de la
aplicación con una propiedad
eliminado. Es decir, quieres una
acción para marcar como borrada, en lugar de borrarla de la base de datos,
no solo facturas sino cualquier entidad. En este caso, el código actual de
tu acción no es suficiente. Necesitas un código más genérico.
Puedes conseguir una acción más genérica usando la clase de OpenXava
llamada
MapFacade.
MapFacade (del paquete
org.openxava.model)
te permite manejar el estado de tus entidades usando mapas, esto es
conveniente ya que
View trabaja con mapas. Además, los mapas son
más dinámicos que los objetos y por tanto más apropiados para crear código
genérico.
Reescribamos nuestra acción para borrar. Primero, renombremos
EliminarFactura
(una acción para borrar objetos de tipo
Factura) como
EliminarParaFacturacion
(la acción para borrar objetos en la aplicación
Facturacion).
Esto implica que tienes que cambiar la entrada para la acción en
controladores.xml,
para cambiar el nombre de la clase. Tal como se muestra a continuación:
<accion nombre="delete"
modo="detail" confirmar="true"
clase="com.tuempresa.facturacion.acciones.EliminarParaFacturacion"
icono="delete"
disponible-en-nuevo="false"
atajo-de-teclado="Control D"/>
Ahora, renombra tu
EliminarFactura como
EliminarParaFacturacion
y reescribe su código:
package com.tuempresa.facturacion.acciones;
import java.util.*;
import org.openxava.actions.*;
import org.openxava.model.*;
public class EliminarParaFacturacion extends ViewBaseAction {
public void execute() throws Exception {
Map<String, Object> valores =
new HashMap<>(); // Los valores a modificar en la entidad
valores.put("eliminado", true); // Asignamos true a la propiedad 'eliminado'
MapFacade.setValues( // Modifica los valores de la entidad indicada
getModelName(), // Un método de ViewBaseAction
getView().getKeyValues(), // La clave de la entidad a modificar
valores); // Los valores a cambiar
resetDescriptionsCache(); // Reinicia los caches para los combos
addMessage("object_deleted", getModelName());
getView().clear();
getView().setKeyEditable(true);
getView().setEditable(false); // Dejamos la vista como no editable
}
}
Esta acción hace lo mismo que la anterior, pero no tiene ninguna
referencia a la entidad
Factura. Por tanto, es genérica, puedes
usarla con
Pedido,
Autor o cualquier otra entidad
siempre y cuando tenga una propiedad
eliminado. El truco está en
MapFacade la cual permite modificar una entidad a partir de mapas.
Puedes obtener esos mapas directamente de la vista (usando
getView().getKeyValues()
por ejemplo) o puedes crearlos de una manera genérica, como en el caso del
mapa
valores.
Adicionalmente puedes ver dos pequeñas mejoras sobre la versión antigua.
Primero, llamamos a
resetDescriptionsCache(), un método de
BaseAction.
Este método borra el caché usado para los combos. Cuando modificas una
entidad, si quieres que los combos reflejen los cambios en la sesión
actual has de llamar a este método. Segundo, llamamos a
getView().setEditable(false).
Esto inhabilita los controles de la vista, para impedir que el usuario
rellene datos en la vista. Para crear una nueva entidad el usuario tiene
que pulsar el botón
Nuevo.
Ahora tu acción está lista para ser usada por cualquier otra entidad.
Podríamos copiar y pegar el controlador
Factura como
Pedido
en
controladores.xml. De esta forma, nuestra lógica genérica
para borrar se usaría para
Pedido. ¡Espera un momento! ¿He dicho
“copiar y pegar”? No queremos arder en el fuego eterno del infierno,
¿verdad? Así que usaremos una forma más automática de insuflar nuestra
nueva acción a todos lo módulos. Aprendámoslo en la siguiente sección.
Cambiar
el controlador por defecto para todos los módulos
Si usas
EliminarParaFacturacion solo para
Factura
entonces definirla en el controlador
Factura de
controladores.xml
es una buena táctica. Pero, recuerda que hemos mejorado esta acción
precisamente para hacerla reutilizable, por tanto reutilicémosla. Vamos a
asignar un controlador a todos los módulos de un solo golpe.
El primer paso es cambiar el nombre del controlador de
Factura a
Facturacion:
<controlador nombre="Facturacion">
<hereda-de controlador="Typical"/>
<accion nombre="delete" modo="detail" confirmar="true"
clase="com.tuempresa.facturacion.acciones.EliminarParaFacturacion"
icono="delete"
disponible-en-nuevo="false"
atajo-de-teclado="Control D"/>
</controlador>
Como ya sabes, cuando usas el nombre de una entidad, como
Factura,
como nombre de controlador, ese controlador será usado por defecto en el
módulo de esa entidad. Por lo tanto, si cambiamos el nombre del
controlador, este controlador no se usará para la entidad. De hecho el
controlador
Facturacion no es usado por ningún módulo, porque no
hay ninguna entidad llamada "Facturacion".
Queremos que el controlador
Facturacion sea el controlador usado
por defecto por todos los módulos de la aplicación. Para hacer esto hemos
de modificar el archivo
aplicacion.xml que tienes en la carpeta
xava de tu aplicación. Dejándolo así:
<?xml version = "1.0" encoding = "ISO-8859-1"?>
<!DOCTYPE aplicacion SYSTEM "dtds/aplicacion.dtd">
<aplicacion nombre="Facturacion">
<!--
Se asume un módulo por defecto para cada entidad con el
controlador de <modulo-defecto/>
-->
<modulo-defecto>
<controlador nombre="Facturacion" />
</modulo-defecto>
</aplicacion>
De esta forma tan simple todos los módulos de tu aplicación ahora
usarán
Facturacion en lugar de
Typical como
controlador por defecto. Trata de ejecutar tu módulo
Factura y
verás como la acción se ejecuta al borrar un elemento.
Puedes probar el módulo
Pedido también, pero no funcionará
porque no tiene la propiedad
eliminado. Podríamos añadir la
propiedad
eliminado a
Pedido y funcionaría con nuestro
nuevo controlador, pero en vez de “copiar y pegar” la propiedad
eliminado
en todas nuestras entidades, vamos a usar una técnica mejor. Veámoslo en
la siguiente sección.
Volvamos
un momento al modelo
Tu tarea ahora sería añadir la propiedad
eliminado a todas las
entidades para que la
EliminarParaFacturacion funcione. Esta es
una buena ocasión para usar herencia y así poner el código común en el
mismo sitio, en lugar de usar el infame “copiar y pegar”.
Primero quita la propiedad
eliminado de
Factura:
public class Factura extends DocumentoComercial {
//@Hidden // No se mostrará por defecto en las vistas y los tabs
//@Column(columnDefinition="BOOLEAN DEFAULT FALSE")
//boolean eliminado;
// El resto del código...
}
Y ahora crea una nueva superclase mapeada llamada
Eliminable
en el paquete
com.tuempresa.facturacion.modelo:
package com.tuempresa.facturacion.modelo;
import javax.persistence.*;
import org.openxava.annotations.*;
import lombok.*;
@MappedSuperclass @Getter @Setter
public class Eliminable extends Identificable {
@Hidden
@Column(columnDefinition="BOOLEAN DEFAULT FALSE")
boolean eliminado;
}
Eliminable es una superclase mapeada. Recuerda, una
superclase mapeada no es una entidad, es una clase con propiedades,
métodos y anotaciones de mapeo para ser usada como superclase para
entidades.
Eliminable extiende de
Identificable, por
tanto cualquier entidad que extienda
Eliminable tendrá las
propiedades
oid y
eliminado.
Ahora puedes convertir cualquiera de tus entidades actuales en
Eliminable,
solo has de cambiar
Identificable por
Eliminable como
superclase. Hagámoslo con
DocumentoComercial:
// abstract public class DocumentoComercial extends Identificable {
abstract public class DocumentoComercial extends Eliminable {
// El resto del código...
}
Dado que
Factura y
Pedido son
DocumentoComercial,
ahora puedes usar tu controlador
Facturacion con la
ElminarParaFaturacion
contra ellos.
Nos queda un sutil detalle. La entidad
Pedido tiene un método
@PreRemove
para hacer una validación al borrar. Esta validación puede impedir el
borrado. Podemos mantener esta validación para nuestro borrado
personalizado simplemente sobrescribiendo el método
setEliminado()
de
Pedido:
public class Pedido extends DocumentoComercial {
// ...
@PreRemove
private void validarPreBorrar() { // Ahora este método no se ejecuta
if (factura != null) { // automáticamente ya que el borrado real no se produce
throw new javax.validation.ValidationException(
XavaResources.getString("no_puede_borrar_pedido_con_factura"));
}
}
public void setEliminado(boolean eliminado) {
if (eliminado) validarPreBorrar(); // Llamamos a la validación explícitamente
super.setEliminado(eliminado);
}
}
Con este cambio la validación funciona igual que en el caso de un
borrado de verdad, así preservamos el comportamiento original intacto.
Metadatos
para un código más genérico
Con tu actual código de
Factura y
Pedido el
funcionamiento es bueno. Aunque si tratas de borrar una entidad de
cualquier otro módulo, recibirás un feo mensaje de error. La figura
siguiente muestra lo que ocurre cuando intentas borrar un
Cliente.

Sí, si tu entidad no tiene una propiedad
eliminado, la acción de
borrar falla miserablemente. Es verdad que gracias a la clase
Eliminable
puedes añadir la propiedad
eliminado a todas tus entidades
fácilmente, pero puede ser que quieras tener entidades que puedan marcarse
como borradas (
Eliminable) y entidades que sean borradas de
verdad de la base de datos. Queremos que la acción funcione bien en todos
los casos.
OpenXava almacena metadatos para todas tus entidades y puedes acceder a
estos metadatos desde tu código. Esto te permite, por ejemplo, averiguar
si la entidad tiene una propiedad
eliminado.
El siguiente código muestra una modificación en la acción para preguntar
si la entidad tiene una propiedad
eliminado, si no el proceso de
borrado no se realiza:
public void execute() throws Exception {
if (!getView().getMetaModel() // Metadatos de la entidad actual
.containsMetaProperty("eliminado")) // ¿Tiene una propiedad 'eliminado'?
{
addMessage( // De momento, mostramos un mensaje si la propiedad 'eliminado' no está
"No eliminado, ésta no tiene propiedad eliminado");
return;
}
// El resto del código...
}
La clave aquí es
getView().getMetaModel() que devuelve un
objeto
MetaModel del paquete
org.openxava.model.meta. Este
objeto contiene metadatos sobre la entidad actualmente visualizada en la
vista. Puedes preguntar por propiedades, referencias, colecciones, métodos
y otra metainformación sobre la entidad. Consulta la
API
de MetaModel para aprender más. En este caso preguntamos si
la propiedad
eliminado existe.
De momento solo mostramos un mensaje. Mejorémoslo para borrar de verdad la
entidad.
Llamar
a otra acción desde una acción
Queremos que cuando la entidad no tenga una propiedad
eliminado
sea borrada de la base de datos de la manera habitual. Nuestra primera
opción es escribir nosotros mismos la lógica de borrado, realmente no es
una tarea complicada. Sin embargo, es mucho mejor usar la lógica estándar
de borrado de OpenXava, así no necesitamos escribir ninguna lógica de
borrado y usamos un código más refinado y probado.
Para hacer esto OpenXava permite llamar a una acción desde dentro de una
acción, simplemente llama a
executeAction() indicando el nombre
calificado de la acción, es decir, el nombre del controlador y el nombre
de la acción. En nuestro caso para llamar a la acción estándar de
OpenXava para borrar usaríamos
executeAction("CRUD.delete"). El
siguiente código muestra
EliminarParaFacturacion modificada para
llamar a la acción estándar de OpenXava para borrar.
package com.tuempresa.facturacion.acciones;
import java.util.*;
import org.openxava.actions.*;
import org.openxava.model.*;
public class EliminarParaFacturacion extends ViewBaseAction {
public void execute() throws Exception {
if (!getView().getMetaModel().containsMetaProperty("eliminado")) {
executeAction("CRUD.delete"); // LLamamos a la acción estándar
return; // de OpenXava para borrar
}
// Cuando "eliminado" existe usamos nuestra propia lógica de borrado
Map<String, Object> valores = new HashMap<>();
valores.put("eliminado", true);
MapFacade.setValues(getModelName(), getView().getKeyValues(), valores);
resetDescriptionsCache();
addMessage("object_deleted", getModelName());
getView().clear();
getView().setKeyEditable(true);
getView().setEditable(false);
}
}
Simplemente llamamos a
executeAction(“CRUD.delete”) si
queremos que la acción por defecto para borrar de OpenXava se ejecute.
Así, escribimos nuestra propia lógica de borrado (en este caso marcar una
propiedad con
true) para algunos casos y “dejamos pasar” la lógica
estándar para los demás.
Ahora puedes usar tu
EliminarParaFacturacion contra cualquier
entidad. Si la entidad tiene una propiedad
eliminado se marcará
como borrada, en caso contrario se borrará físicamente de la base de
datos.
Este ejemplo te muestra como usar
executeAction() para refinar la
lógica estándar de OpenXava. Otra forma de hacerlo es mediante la
herencia. Veamos cómo en la siguiente sección.
Refinar
la acción de búsqueda por defecto
EliminarParaFacturacion ahora funciona bastante bien, aunque no
tiene demasiada utilidad. Es inútil marcar como borrados los objetos, si
el resto de la aplicación no es consciente de ello. Es decir, hemos de
modificar otras partes de la aplicación para que traten los objetos
“marcados como borrados” como si no existieran.
El lugar más obvio para empezar es la acción de búsqueda. Si borras una
factura y después tratas de buscarla, no deberías encontrarla. La
siguiente figura muestra como funciona la búsqueda en OpenXava.

La primera cosa que puedes observar en la figura anterior es que buscar en
modo detalle es más flexible de lo que parece. El usuario puede introducir
cualquier valor en cualquier campo, o combinación de campos, y pulsar en
el botón de refrescar. Entonces el primer objeto cuyos valores coinciden
es cargado en la vista.
Puedes pensar: Bueno, puedo refinar la acción
CRUD.refresh de la
misma forma que he refinado
CRUD.delete. Por supuesto, puedes
hacerlo así. Y funcionaría; cuando el usuario pulsara en la acción del
modo detalle tu código se ejecutaría. Aunque, aquí hay un detalle un tanto
sutil. La lógica de buscar no se llama sólo desde el modo detalle, sino
también desde otros puntos del módulo OpenXava. Por ejemplo, cuando el
usuario escoge un detalle, la acción
List.viewDetail coge la
clave de la fila, la pone en la vista de detalle y después ejecuta la
acción de buscar.
Para hacerlo bien, hemos de poner la lógica para buscar en un módulo, en
la misma acción, y todas las acciones que necesiten buscar encadenarán con
esta acción. Tal como muestra la anterior figura.
Esto queda más claro si ves el código de la acción estándar
CRUD.refresh,
que es
org.openxava.actions.SearchAction cuyo código se muestra a
continuación:
public class SearchAction extends BaseAction
implements IChainAction { // Encadena con otra acción
public void execute() throws Exception { // No hace nada
}
public String getNextAction() throws Exception { // De IChainAction
return getEnvironment() // Para acceder a las variables de entorno
.getValue("XAVA_SEARCH_ACTION");
}
}
Como ves, la acción estándar para buscar en modo detalle no hace
nada, simplemente redirige a otra acción. Esta otra acción se define en
una variable de entorno llamada
XAVA_SEARCH_ACTION, que lee
usando
getEnvironment(). Por la tanto, si quieres refinar la
lógica de búsqueda de OpenXava la mejor manera es definiendo tu acción
como valor para
XAVA_SEARCH_ACTION. Hagámoslo pues de esta
manera.
Para dar valor a la variable de entorno edita el archivo
controladores.xml
en la carpeta
xava de tu proyecto y añade al principio la línea
<var-entorno
/> como ves a continuación:
...
<controladores>
<!-- Para definir un valor global para una variable de entorno -->
<var-entorno
nombre="XAVA_SEARCH_ACTION"
valor="Facturacion.buscarExcluyendoEliminados" />
<controlador nombre="Facturacion">
...
De esta forma el valor para la variable de entorno
XAVA_SEARCH_ACTION
en cualquier módulo será “Facturacion.buscarExcluyendoEliminados”, por lo
tanto la lógica de búsqueda para todos los módulos estará en esta acción.
El siguiente paso lógico es definir esta acción en el controlador
"Facturacion" del mismo
controladores.xml:
<controlador nombre="Facturacion">
...
<accion nombre="buscarExcluyendoEliminados"
oculta="true"
clase="com.tuempresa.facturacion.acciones.BuscarExcluyendoEliminados" />
<!-- oculta="true" : Así la acción no se mostrará en la barra de botones -->
</controlador>
Y ahora es el momento para escribir la clase de implementación. En
este caso solo queremos refinar la lógica de búsqueda, es decir, la
búsqueda se ha de hacer de la forma convencional, con la excepción de las
entidades con una propiedad
eliminado cuyo valor sea
true.
Para hacer este refinamiento vamos a usar herencia. El siguiente código
muestra la acción:
package com.tuempresa.facturacion.acciones;
import java.util.*;
import javax.ejb.*;
import org.openxava.actions.*;
public class BuscarExcluyendoEliminados
extends SearchByViewKeyAction { // La acción estándar de OpenXava para buscar
private boolean esEliminable() { // Pregunta si la entidad tiene una propiedad 'eliminado'
return getView().getMetaModel()
.containsMetaProperty("eliminado");
}
protected Map getValuesFromView() // Coge los valores visualizados desde la vista
throws Exception // Estos valores se usan como clave al buscar
{
if (!esEliminable()) { // Si no es 'eliminable' usamos la lógica estándar
return super.getValuesFromView();
}
Map<String, Object> valores = super.getValuesFromView();
valores.put("eliminado", false) ; // Llenamos la propiedad 'eliminado' con false
return valores;
}
protected Map getMemberNames() // Los miembros a leer de la entidad
throws Exception
{
if (!esEliminable()) { // Si no es 'eliminable' ejecutamos la lógica estándar
return super.getMemberNames();
}
Map<String, Object> miembros = super.getMemberNames();
miembros.put("eliminado", null); // Queremos obtener la propiedad 'eliminado'
return miembros; // aunque no esté en la vista
}
protected void setValuesToView(Map valores) // Asigna los valores desde
throws Exception // la entidad a la vista
{
if (esEliminable() && // Si tiene una propiedad 'eliminado' y
(Boolean) valores.get("eliminado")) { // vale true
throw new ObjectNotFoundException(); // lanzamos la misma excepción que
// OpenXava lanza cuando el objeto no se encuentra
}
else {
super.setValuesToView(valores); // En caso contrario usamos la lógica estándar
}
}
}
La lógica estándar para buscar está en la clase
SearchByViewKeyAction.
Básicamente, la lógica de esta clase consiste en coger los valores de la
vista, si la propiedad
id está presente buscará por id, en caso
contrario coge todos los valores en la vista para usar en la condición de
búsqueda, devolviendo el primer objeto que coincida con la condición.
Queremos usar este mismo algoritmo cambiando solo algunos detalles sobre
la propiedad
eliminado. Por tanto, en vez de sobrescribir el
método
execute(), que contiene la lógica de búsqueda,
sobrescribimos tres métodos protegidos, que son llamados desde
execute()
y contienen algunos puntos susceptibles de ser refinados.
Después de estos cambios prueba tu aplicación, y verás como cuando tratas
de buscar una factura o un pedido, si están borrados no se muestran.
Incluso si escoges una factura o pedido borrado desde el modo lista se
producirá un error y no verás los datos en modo detalle.
Has visto como al definir una variable de entorno
XAVA_SEARCH_ACTION
en
controladores.xml estableces la lógica de búsqueda de una
manera global, es decir, para todos los módulos a la vez. Si lo que
quieres es definir una acción de búsqueda para un módulo en particular,
simplemente define la variable de entorno en la definición del módulo en
aplicacion.xml,
tal como mostramos a continuación:
<modulo nombre="Producto">
<!--Para dar un valor local a la variable de entorno para este módulo -->
<var-entorno
nombre="XAVA_SEARCH_ACTION"
valor="Producto.buscarPorNumero"/>
<modelo nombre="Producto"/>
<controlador nombre="Producto"/>
<controlador nombre="Facturacion"/>
</modulo>
De esta forma para el módulo
Producto la variable de
entorno
XAVA_SEARCH_ACTION valdrá
“Producto.buscarPorNumero”.
Es decir, las variables de entorno son locales a los módulos. Aunque
definas un valor por defecto en
controladores.xml, siempre
tienes la opción de sobrescribirlo para un módulo concreto. La variables
de entorno son una forma práctica de configurar tu aplicación
declarativamente.
No queremos una forma especial de búsqueda para
Producto, por
tanto no añadas esta definición de módulo a tu
aplicacion.xml.
Este código solo era para ilustrar el uso de
<var-entorno />
en los módulos.
Modo lista
Ya casi tenemos el trabajo hecho. Cuando el usuario borra una entidad con
una propiedad
eliminado la entidad se marca como borrada en vez
de ser borrada físicamente de la base de datos. Y si el usuario trata de
buscar una entidad “marcada como borrada” no puede verla en modo detalle.
Aunque, el usuario todavía puede ver las entidades “marcadas como
borradas” en modo lista, y lo que es peor si borra las entidades desde
modo lista, éstas son efectivamente borradas de la base de datos. Atemos
estos cabos sueltos.
Filtrar
datos tabulares
Solo las entidades con su propiedad
eliminado igual a
false
tienen que ser mostradas en modo lista. Esto es muy fácil de conseguir
usando la anotación
@Tab. Esta anotación te permite definir la
forma en que los datos tabulares (los datos mostrados en modo lista) son
visualizados y te permite además definir una condición. Por tanto, añadir
esta anotación a las entidades que tengan una propiedad
eliminado
es suficiente para conseguir nuestro objetivo, tal como se muestra a
continuación:
@Tab(baseCondition = "eliminado = false")
public class Factura extends DocumentoComercial { ... }
@Tab(baseCondition = "eliminado = false")
public class Pedido extends DocumentoComercial { ... }
Y de esta forma tan sencilla el modo lista no mostrará las
entidades “marcadas como borradas”.
Acciones de
lista
El único detalle que nos queda es el borrar las entidades desde modo
lista, éstas han de marcarse como borradas si procede. Vamos a refinar las
acciones estándares
CRUD.deleteSelected y
CRUD.deleteRow
de la misma manera que hemos hecho con
CRUD.delete.
Primero, sobrescribimos la acciones
deleteSelected y
deleteRow
para nuestra aplicación. Añade la siguiente definición de acción a tu
controlador
Facturacion definido en
controladores.xml:
<controlador nombre="Facturacion">
<hereda-de controlador="Typical"/>
<!-- ... -->
<accion nombre="deleteSelected" modo="list" confirmar="true"
procesar-elementos-seleccionados="true"
icono="delete"
clase="com.tuempresa.facturacion.acciones.EliminarSeleccionadoParaFacturacion"
atajo-de-teclado="Control D"/>
<accion nombre="deleteRow" modo="NONE" confirmar="true"
clase="com.tuempresa.facturacion.acciones.EliminarSeleccionadoParaFacturacion"
icono="delete"
en-cada-fila="true"/>
</controlador>
La acciones estándar para borrar entidades desde modo lista son
deleteSelected
(para borrar las filas seleccionadas) y
deleteRow (la acción que
aparece en cada fila). Estas acciones están definidas en el controlador
CRUD.
Typical extiende de
CRUD y
Facturacion
extiende
Typical; así que el controlador
Facturacion
incluye por defecto estas acciones. Dado que hemos definido unas acciones
con los mismos nombres, nuestras acciones sobrescriben las estándares. Es
decir, de ahora en adelante la lógica para borrar las filas seleccionadas
en modo lista está en la clase
EliminarSeleccionadoParaFacturacion.
Fíjate como la lógica para ambas acciones están en una única clase Java.
El código es el siguiente:
package com.tuempresa.facturacion.acciones;
import org.openxava.actions.*;
import org.openxava.model.meta.*;
public class EliminarSeleccionadoParaFacturacion
extends TabBaseAction // Para trabajar con datos tabulares (lista) por medio de getTab()
implements IChainActionWithArgv { // Encadena con otra acción, indicada con getNextAction()
private String nextAction = null; // Para almacenar la siguiente acción a ejecutar
public void execute() throws Exception {
if (!getMetaModel().containsMetaProperty("eliminado")) {
nextAction="CRUD.deleteSelected"; // 'CRUD.deleteSelected' se ejecutará
// cuando esta acción finalice
return;
}
marcarEntidadesSeleccionadasComoEliminadas(); // La lógica para marcar las
// filas seleccionadas como objetos borrados
}
private MetaModel getMetaModel() {
return MetaModel.get(getTab().getModelName());
}
public String getNextAction() // Obligatorio por causa de IChainAction
throws Exception
{
return nextAction; // Si es nulo no se encadena con ninguna acción
}
public String getNextActionArgv() throws Exception {
return "row=" + getRow(); // Argumento a enviar a la la acción encadenada
}
private void marcarEntidadesSeleccionadasComoEliminadas() throws Exception {
// ...
}
}
Puedes ver como esta acción es bastante parecida a
EliminarParaFacturacion.
Si las entidades no tienen la propiedad
eliminado encadena con
la acción estándar, en caso contrario ejecuta su propia lógica para borrar
las entidades. Aunque en este caso usamos
IChainActionWithArgv en
lugar del más sencillo
executeAction() porque necesitamos enviar
un argumento a la acción encadenada. Generalmente las acciones para modo
lista extienden de
TabBaseAction, así puedes usar
getTab()
para obtener el objeto
Tab asociados a la lista. Un
Tab
(de
org.openxava.tab) te permite manipular los datos tabulares.
Por ejemplo en el método
getMetaModel() preguntamos al
Tab
el nombre del modelo para obtener el
MetaModel correspondiente.
Si la entidad tiene una propiedad
eliminado entonces se ejecuta
nuestra propia lógica de borrado. Esta lógica está en el método
marcarEntidadesSeleccionadasComoEliminadas()
que puedes ver a continuación:
private void marcarEntidadesSeleccionadasComoEliminadas() throws Exception {
Map<String, Object> valores = new HashMap<>(); // Valores a asignar a cada entidad para marcarla
valores.put("eliminado", true); // Pone 'eliminado' a true
Map<String, Object>[] clavesSeleccionadas = getSelectedKeys(); // Obtenemos las filas seleccionadas
if (clavesSeleccionadas != null) {
for (int i = 0; i < clavesSeleccionadas.length; i++) { // Iteramos sobre las filas seleccionadas
Map<String, Object> clave = clavesSeleccionadas[i]; // Obteniendo la clave de cada entidad
try {
MapFacade.setValues( // Modificamos cada entidad
getTab().getModelName(),
clave,
valores);
}
catch (javax.validation.ValidationException ex) { // Si se produce una ValidationException..
addError("no_delete_row", i, clave);
addError("remove_error", getTab().getModelName(), ex.getMessage()); // ...mostramos el mensaje
}
catch (Exception ex) { // Si se lanza cualquier otra excepción, se añade
addError("no_delete_row", i, clave); // un mensaje genérico
}
}
}
getTab().deselectAll(); // Después de borrar deseleccionamos la filas
resetDescriptionsCache(); // Y reiniciamos el caché de los combos para este usuario
}
Como ves la lógica es un simple bucle sobre las claves de las
filas seleccionadas, y en cada iteración ponemos a
true la
propiedad
eliminado usando el método
MapFacade.setValues().
Atrapamos las excepciones dentro de la iteración del bucle, así si hay
algún problema borrando la entidad, esto no afecta al borrado de las demás
entidades. Hemos hecho un pequeño refinamiento para el caso de
ValidationException,
añadiendo el mensaje de validación (
ex.getMessage()) a los errores
a mostrar al usuario.
Al final deseleccionamos todas las filas mediante
getTab().deselectAll(),
porque estamos borrando filas, por tanto si no eliminamos la selección,
esta se habría recorrido después de la ejecución de la acción.
Hemos llamado a
resetDescriptionsCache() para actualizar las
entidades borradas en todos los combos de la actual sesión de usuario. Los
combos, es decir las referencias marcadas con
@DescriptionsList,
usan el
@Tab de la entidad referenciada para filtrar los datos. Es
decir, si tuvieras un combo de facturas o pedidos con la condición
“deleted
= false” en el
@Tab, en este caso el contenido del combo
cambiaría.
Ahora ya tienes refinada del todo la forma en que tu aplicación borra las
entidades. Aunque aún nos quedan cosas interesantes por hacer.
Reutilizar
el código de las acciones
Ahora tu aplicación marca como borradas las facturas y pedidos en vez de
borrarlos. La ventaja de este enfoque es que el usuario puede restaurar en
cualquier momento una factura o pedido borrado por error. Para que esta
característica sea útil de verdad has de proporcionar al usuario una
herramienta para restaurar las entidades borradas. Vamos a crear un módulo
papelera para
Factura y otro para
Pedido para traer
los documentos borrados de vuelta a la vida.
Propiedades
para crear acciones reutilizables
La papelera que queremos es como la que puedes ver en la siguiente figura.
Es una lista de facturas o pedidos donde el usuario pueda seleccionar
varias y pulsar en el botón
Restaurar, o simplemente pulsar en
el vínculo
Restaurar en la fila del documento que quiera
restaurar:

La lógica de esta acción de restaurar es simplemente poner la propiedad
eliminado
de las entidades seleccionadas a
false. Es decir, es exactamente
la misma lógica que usamos para borrar, pero poniendo
false en
vez de
true. Dado que nuestra conciencia no nos permite copiar y
pegar, vamos a reutilizar nuestro código actual. La forma de reutilizar es
añadiendo una propiedad
restaurar a la acción
EliminarSeleccionadoParaFacturacion,
para poder restaurar las entidades borradas.
El siguiente código muestra lo necesario para añadir una propiedad
restaurar
a la acción:
public class EliminarSeleccionadoParaFacturacion ... {
//...
@Getter @Setter
boolean restaurar; // Una nueva propiedad
private void marcarEntidadesSeleccionadasComoEliminadas() throws Exception {
Map<String, Object> valores = new HashMap<String, Object>();
// valores.put("eliminado", true); // Pone 'eliminado' a true // En lugar de un true fijo, usamos
valores.put("eliminado", !isRestaurar()); // el valor de la propiedad 'restaurar';
// ...
}
Como puedes ver solo hemos añadido una propiedad
restaurar
y el uso de su complemento como nuevo valor para la propiedad
eliminado
en la entidad. Es decir, si
restaurar es
false, el
caso por defecto, un
true se grabará en
eliminado, así
tu acción de borrar borrará. Pero si
restaurar es
true
la acción guardará
false en la propiedad
eliminado de
la entidad, y por tanto la factura, pedido o cualquier otra entidad estará
de nuevo disponible en la aplicación.
Para usar esta acción como una acción para restaurar has de definirla en
controladores.xml,
tal como muestra el siguiente código:
<controlador nombre="Papelera">
<accion nombre="restaurar" modo="list"
clase="com.tuempresa.facturacion.acciones.EliminarSeleccionadoParaFacturacion">
<poner propiedad="restaurar" valor="true"/> <!-- Pone la propiedad restaurar a true -->
<!-- antes de llamar al método execute() de la acción -->
</accion>
</controlador>
A partir de ahora puedes referenciar a la acción
Papelera.restaurar
cuando necesites una acción para restaurar. Estás reutilizando el mismo
código para borrar y restaurar, gracias al elemento
<poner />
de
<accion /> que te permite configurar las propiedades de
la acción.
Usemos esta nueva acción de restaurar en los nuevos módulos papelera.
Módulos
personalizados
Como ya sabes, OpenXava genera un módulo por defecto para cada entidad de
tu aplicación. Aunque, siempre tienes la opción de definir los módulos a
mano, bien para refinar el comportamiento del módulo para cierta entidad,
o bien para definir una funcionalidad completamente nueva sobre esa
entidad. En este caso vamos a crear dos nuevos módulos,
PapeleraFactura
y
PapeleraPedido, para restaurar los documentos borrados.
Usaremos el controlador
Papelera en ellos. El siguiente código
muestra la definición de módulos en el archivo
aplicacion.xml:
<aplicacion nombre="Facturacion">
<modulo-defecto>
<controlador nombre="Facturacion"/>
</modulo-defecto>
<modulo nombre="PapeleraFactura">
<var-entorno nombre="XAVA_LIST_ACTION"
valor="Papelera.restaurar"/> <!-- La acción a mostrar en cada fila -->
<modelo nombre="Factura"/>
<tab nombre="Eliminado"/> <!-- Para mostrar solo las entidades borradas -->
<controlador nombre="Papelera"/> <!-- Con solo una acción: restaurar -->
</modulo>
<modulo nombre="PapeleraPedido">
<var-entorno nombre="XAVA_LIST_ACTION" valor="Papelera.restaurar"/>
<modelo nombre="Pedido"/>
<tab nombre="Eliminado"/>
<controlador nombre="Papelera"/>
</modulo>
</aplicacion>
Estos módulos van contra
Factura y
Pedido,
pero definen una acción especial como acción de fila usando la variable de
entorno
XAVA_LIST_ACTION. La siguiente figura muestra
PapeleraFactura:
Varias
definiciones de datos tabulares por entidad
Otro detalle importante es que solo las entidades borradas se muestran en
la lista. Esto es posible porque definimos un
@Tab específico
indicando su nombre para el módulo. El siguiente código detalla como
escoger el
@Tab para un módulo:
<modulo nombre="...">
...
<tab nombre="Eliminado"/> <!-- "Eliminado" es un @Tab definido en la entidad -->
...
</modulo>
Por supuesto, has de tener un
@Tab llamado “Eliminado” en
tus entidades
Pedido y
Factura. Tal como se muestra a
continuación:
@Tab(baseCondition = "eliminado = false") // Tab sin nombre, es el de por defecto
@Tab(name="Eliminado", baseCondition = "eliminado = true") // Tab con nombre
public class Factura extends DocumentoComercial { ... }
@Tab(baseCondition = "eliminado = false")
@Tab(name="Eliminado", baseCondition = "eliminado = true")
public class Pedido extends DocumentoComercial { ... }
Usamos el
@Tab sin nombre como lista por defecto para
Factura
y
Pedido, pero tenemos un
@Tab llamado
"Eliminado"
que puedes usar para generar una lista con solo las filas borradas. En
este caso lo usamos para los módulos papelera. Ahora puedes probar tus
nuevos módulos, si no los ves en el menú prueba cerrar sesión y volver a
identificarte.
Obsesión
por reutilizar
¡Bien hecho! El código de
EliminarSeleccionadoParaFacturacion
puede borrar y restaurar entidades, y hemos añadido la capacidad de
restaurar con solo un poco más de código, sin copiar y pegar.
Y ahora un enjambre de perniciosos pensamientos bullen en tu cabeza.
Seguramente estés pensando “Esta acción no es únicamente para borrar, sino
también para borrar y restaurar”, y entonces, “Espera un momento, lo que
es en realidad es una acción para actualizar la propiedad
eliminado
de la entidad actual”, y tu siguiente pensamiento será “Con tan solo un
poco más podemos actualizar cualquier propiedad de la entidad”.
Sí, estás en lo cierto. Con facilidad podemos crear una acción más
genérica, una
ActualizarPropiedad por ejemplo, y usarla para
declarar tus acciones
deleteSelected y
restaurar, tal
como se muestra a continuación:
<accion nombre="deleteSelected" modo="list" confirmar="true"
class="com.tuempresa.facturacion.acciones.ActualizarPropiedad"
atajo-de-teclado="Control D">
<poner propiedad="propiedad" valor="eliminado" />
<poner propiedad="valor" valor="true" />
</accion>
<accion nombre="restaurar" modo="list"
class="com.tuempresa.facturacion.acciones.ActualizarPropiedad">
<poner propiedad="propiedad" valor="eliminado" />
<poner propiedad="valor" valor="false" />
</accion>
Aunque parezca una buena idea, no vamos a crear esta flexible
ActualizarPropiedad.
Porque cuanto más flexible sea tu código, más sofisticado será. Y no
queremos código sofisticado. Queremos código sencillo, y aunque el código
sencillo es algo imposible de conseguir, hemos de esforzarnos por que
nuestro código sea lo más sencillo posible. El consejo es: crea código
reutilizable solo cuando éste simplifique tu aplicación en el presente.
Resumen
El comportamiento estándar de OpenXava solo es el punto de partida. Usando
la acción de borrar como excusa, hemos explorado algunas formas de refinar
los detalles del comportamiento de la aplicación. Con las técnicas de esta
lección no solo puedes refinar la lógica de borrado, sino también definir
completamente la forma en que una aplicación OpenXava funciona. Así,
tienes la posibilidad de adaptar el comportamiento de tu aplicación para
cubrir las expectativas de tus usuarios.
El comportamiento por defecto de OpenXava es limitado: solo mantenimientos
y listados. Si quieres una aplicación que de verdad aporte valor a tu
usuario necesitas añadir funcionalidad específica que le ayude a resolver
sus problemas. Haremos esto en la próxima lección.
Descargar código fuente de esta lección
¿Problemas con la lección? Pregunta en el foro ¿Ha ido bien?
Ve a la
lección 20