AspectJ: Estado de un botón condicionado por campos de texto

Empecé hace poco a interesarme por la programación orientada a aspectos (AOP) y me daba vueltas por la cabeza si podría servir para solucionar el problema que se nos presenta cuando diseñamos un formulario de entrada de datos en el que hay un botón, o varios, cuyo estado activo/inactivo depende de si se han introducido todos los datos o no. En programación orientada a objetos se suele resolver creando un listener para los eventos OnModify/OnChange u OnExit/OnFocusLost de cada componente de texto desde donde se llama a alguna función que comprueba si todos los campos han sido cumplimentados. Esto suele implicar la inicialización de alguna lista para poder recorrerla después y poder comprobar los contenidos. En pseudocódigo:
función estadoBoton()
variable = true;
mientras (existen items en lista y variable sea true)
variable = longitud texto lista[item] > 0;
fin mientras
botonOK = variable;
fin función

crear campoTexto1;
campoTexto1.OnModify = estadoBoton();
crear campoTexto2;
campoTexto2.OnModify = estadoBoton();
crear lista[campoTexto1, campoText2];
crear botonOK;
¿Podemos simplificarlo en AspectJ? Con las pruebas que he hecho opino que la respuesta es afirmativa: podemos crear un aspecto que genere la lista automáticamente al crear los textos, que sepa cual es el botón que debe activar/desactivar y que ejecute la función de comprobación cada vez que se modifique un campo. Veamos cómo. Abrimos Eclipse y creamos un nuevo proyecto AspectJ. A continuación generamos una SWT Application Window e insertamos dos Text y un Button en la ventana, obteniendo algo así:
public class Principal {

 protected Shell shell;
 private Text Text1;
 private Text Text2;
 private Button BtnNewButton;

//... Métodos main y open

 /**
  * Crear contenidos de la ventana.
  */
 protected void createContents() {
  shell = new Shell();
  shell.setSize(450, 300);
  shell.setText("SWT Application");
  
  Text1 = new Text(shell, SWT.BORDER);
  Text1.setBounds(25, 53, 76, 21);
  
  Text2 = new Text(shell, SWT.BORDER);
  Text2.setBounds(25, 102, 76, 21);

  BtnNewButton = new Button(shell, SWT.NONE);
  BtnNewButton.setBounds(152, 53, 75, 25);
  BtnNewButton.setText("New Button");
 }
Es importante que los campos no sean locales, por eso los vemos declarados en el bloque de inicialización de la clase. En este punto necesitamos encontrar join-points en los que nuestro aspecto pueda interceptar el comportamiento de los widgets que nos interesan. Después de varios experimentos fallidos intentando acceder por herencia a los componentes SWT (no permiten subclass) o mediante Reflection descubrí que la mejor opción era crearme mis propios join-points. Necesitamos 3: para saber cuando se crea un Text, para saber cuando se crea el botón y para el evento OnModify de los campos de texto, por lo que modificamos la clase Principal de la siguiente manera:
// Añadimos dos métodos

 private Text crearText()  // Método para crear Text: join-point
 {
   return new Text(shell, SWT.BORDER);
 }
 
 private Button crearButton()  // Método para crear Button: join-point
 {
  return new Button(shell, SWT.NONE);
 }
 
 /**
  * Crear contenidos de la ventana
  */
 protected void createContents() {
  shell = new Shell();
  shell.setSize(450, 300);
  shell.setText("SWT Application");
  
  Text1 = crearText();  // Creamos los Text llamando al método creado
  Text1.addModifyListener(new ModifyListener() {  // Evento OnModify: join-point
   public void modifyText(ModifyEvent arg0) {  // Se deja vacío, se ocupa el aspecto
   }
  });
  Text1.setBounds(25, 53, 76, 21);
  
  Text2 = crearText();  
  Text2.addModifyListener(new ModifyListener() {
   public void modifyText(ModifyEvent arg0) {
   }
  });
  Text2.setBounds(25, 102, 76, 21);

  BtnNewButton = crearButton(); // Creamos el Button llamando al método creado
  BtnNewButton.setBounds(152, 53, 75, 25);
  BtnNewButton.setText("New Button");
 }
Ya podemos pues crear nuestro aspecto con los pointcuts requeridos:

  • loadText() para recoger los Text creados y guardarlos en una lista (listaText)
  • loadButton() para recoger el botón creado
  • ctrlText() para establecer el estado del botón cuando se modifique algún Text (recorre listaText)

Los advice de loadText() y loadButton() recogen el valor devuelto que es en cada caso el componente recien creado que nos interesa. El advice de ctrlText() aplica la función vista anteriormente en pseudocódigo para comprobar si todos los Text han sido rellenados y establece el estado del botón en consecuencia.
public aspect ControlEstado {
 
 List<Text> listaText = new ArrayList<Text>();
 Button btnCtrl = null;

 pointcut loadText() : execution(Text Principal.crearText());
 pointcut loadButton(): execution(Button Principal.crearButton());
 pointcut ctrlText() : execution(public void modifyText(..));
 
 after() returning(Object o) : loadText()
 {
  listaText.add((Text) o);
 }
 
 after() returning(Object o) : loadButton()
 {
  btnCtrl = (Button) o;
  btnCtrl.setEnabled(false);
 }

 before() : ctrlText()
 {
  boolean ret = true;
  int i = 0;
  
  while (i < listaText.size() && ret)
   ret = listaText.get(i++).getText().length() > 0;
  btnCtrl.setEnabled(ret);
    }
}
Con este sencillo aspecto y el código de la clase principal ligeramente modificado conseguimos automatizar la gestión del estado de un botón (o varios) sin perder claridad en el código. Es posible que el pointcut ctrlText() deba refinarse más para no colisionar con otros eventos del mismo tipo generados por otros widgets que no nos interesan, pero no debería ser muy complicado usando por ejemplo anotaciones. Quizás no es muy ortodoxo pero funciona.

Enlaces: AspectJ Homepage, The Aspects Blog, Using AspectJ for Accessing Private Members without Reflection, AOP: AspectJ field access to inject dependencies, Java Reflection (3 partes)

No hay comentarios:

Publicar un comentario