Clases de dominio con imágenes en Grails

30 Sep
2009

Hace ya muchos años, Confucio dijo aquello de:

Lo escuché y lo olvidé, lo ví y lo entendí, lo hice y lo aprendí.

Así que he decidido ponerme manos a la obra y hacer algo pequeño con Grails, algo que sea algo más que una aplicación sencilla con un par de operaciones de CRUD y dejar de leer libros.

Para los que no sepais de lo que estoy hablando, lo que quiero decir es que quiero hacer una aplicación web que no se centre sólo en listar registros de una base de datos, borrarlos y modificarlos, que eso viene en todos los libros. Quiero hacer algo distinto que me obligue a “pegarme” con la tecnología.

Hoy mientras me “pegaba” he encontrado el modo de hacer algo que si bien no es difícil no es trivial y prefiero dejarlo por escrito que no olvidarlo y que caiga en saco roto.

Estoy haciendo algo que se centra en tareas, donde éstas tienen sólo dos campos: la primera es el nombre de la tarea y la segunda el tipo. Las tareas son las clases de dominio y las definimos con Grails tal que así:

class Task {
    String name
    TaskState state
}

Donde TaskState es un tipo enumerado de java:

public enum TaskState {
    INBOX, TODAY, NEXT, IDEA, DONE, TRASH
}

¿Cuál es el problema? Pues que en las páginas quiero mostrar el nombre de la tarea seguido de un icono que variará en función al tipo de la tarea.

De este modo, si la tarea es de tipo ‘inbox’ aparecerá un icono con la forma de una bandeja de entrada seguido de la descripción de la tarea, si es de tipo ‘today’ aparecerá un reloj, y así con los demás.

El problema está en la página gsp, que es la que generará la página html que vemos en los navegadores. Lo primero que piensa uno es “bueno, recupero el tipo de la tarea, y en función a este tipo pinto un icono u otro”, algo como esto:

              <g:if test="${task.state == 'INBOX'}">
                <img src="${resource(dir:'images',file:'inbox.png')}" alt="inbox"/>
              </g:if>
              <g:elseif test="${task.state == 'TODAY'}">
                <img src="${resource(dir:'images',file:'today.png')}" alt="today"/>
              </g:elseif>
              <g:elseif test="${task.state == 'NEXT'}">
                <img src="${resource(dir:'images',file:'next.png')}" alt="next"/>
              </g:elseif>
              <g:elseif test="${task.state == 'IDEA'}">
                <img src="${resource(dir:'images',file:'idea.png')}" alt="idea"/>
              </g:elseif>

Esto no funciona. Si muestras el valor del estado de la tarea verás que es una cadena de texto con el mismo contenido con el que se hace la comprobación, pero no funciona.

He pensado que podia ser por algunas de estas dos razones:
1) Porque comprobar cadenas a este nivel, al igual que ocurre con otras tecnologías como jstl, no es posible
2) Porque en realidad estoy comparando una cadena de texto con un tipo enumerado.

No sé cual será la causa pero lo que sí sé es que hay que buscar una alternativa.

La primera que se me vino a la cabeza, aunque la descarté rápidamente, fue la de llevarme esta comprobación a la parte del programa que se encarga de controlar lo que ocurre en esa página (el controlador :) ). En el controlador podría generar el código html que va en la página y asunto arreglado.

Sin embargo esto es una mala práctica que a la larga dificulta el mantenimiento de las aplicaciones y hace que un cambio que en un futuro debería ser trivial no lo sea tanto.

¿Y cuál es entonces la solución? Bueno, pues hacer uso del patrón “experto” (y lo pongo entre comillas porque como veréis, no es que sea muy experto) y dejar que sea la propia tarea la que responda si se encuentra en un estado u otro.

Es decir, en vez de ir a la tarea y preguntarle “Oye, ¿cuál es tu estado?” , ir a la tarea y decirle “Oye, ¿tu estado es inbox?”, “Oye, ¿tu estado es today?”

Eso lo podemos hacer más o menos como lo haríamos en java, invocando a un método isXXX(), ya que nos va a devolver un valor verdadero/falso.

De esta manera editamos nuestra tarea y ponemos algunos métodos que antes no teniamos:

class Task {
    String name
    TaskState state

    boolean isInInbox() {
        state == TaskState.INBOX
    }

    boolean isInToday() {
        state == TaskState.TODAY
    }

    boolean isInNext() {
        state == TaskState.NEXT
    }

    boolean isInIdea() {
        state == TaskState.IDEA
    }
}

De momento vamos bien. Podemos hacer esas preguntas a la tarea y esta nos devolverá un valor verdadero o falso en función al estado en el que se encuentre y a la pregunta que le hagamos.

Pero me temo que esto tampoco funciona. No funciona porque esta parte de Grails utiliza Hibernate por debajo e Hibernate protesta porque se piensa que tiene que guardar en base de datos algo que no tiene que guardar.

Para entendernos todos, digamos que Hibernate es una tecnología que se utiliza para operar con bases de datos y que cuando ve eso piensa que algo cojea :-)

¿Cuál es la solución?. Decirle a Hibernate que tiene que ignorar esas preguntas, que no van con él, y que deje de intentar almacenarlas y recuperarlas de base de datos. Esto se hace con el ya famoso @Transient pero a lo Grails

class Task {
    String name
    TaskState state

    static transients = [ 'inInbox', 'inToday', 'inNext', 'inIdea' ]

    boolean isInInbox() {
        state == TaskState.INBOX
    }

    boolean isInToday() {
        state == TaskState.TODAY
    }

    boolean isInNext() {
        state == TaskState.NEXT
    }

    boolean isInIdea() {
        state == TaskState.IDEA
    }
}

Una vez salvado este problema ya podemos preguntarle a nuestras tareas en qué estado se encuentran para pintar un icono u otro. Lo haremos de este modo:

               <g:if test="${task.inInbox}">
                <img src="${resource(dir:'images',file:'inbox.png')}" alt="inbox"/>
              </g:if>
              <g:elseif test="${task.inToday}">
                <img src="${resource(dir:'images',file:'today.png')}" alt="today"/>
              </g:elseif>
              <g:elseif test="${task.inNext}">
                <img src="${resource(dir:'images',file:'next.png')}" alt="next"/>
              </g:elseif>
              <g:elseif test="${task.inIdea}">
                <img src="${resource(dir:'images',file:'idea.png')}" alt="idea"/>
              </g:elseif>

Y con esto lograremos poder pintar una imagen que se corresponda con el estado de nuestra clase de dominio.

Espero que tanto unos como otros me perdonéis. A los más técnicos por las cosas que intencionadamente he dicho de modo incompleto para que los menos técnicos lo puedan entender, y a los menos técnicos por las cosas que he dicho que podáis no haber entendido por haber explicado las cosas mal y por encima.

EDITO:

Daniel dió en el clavo en los comentarios :) . He modificado el código y ahora sí que está en lo que yo creo que es su mínima expresión:

La tarea queda así, sin “extras”:

class Task {
    String name
    TaskState state
}

El tipo enumerado sobrecarga el método toString() y pasa a devolver el nombre del tipo enumerado en minúsculas:

public enum TaskState {
    INBOX, TODAY, NEXT, IDEA, DONE, TRASH;

    @Override
    public String toString() {
        return super.toString().toLowerCase();
    }
}

Y ya finalmente nos dejamos de comprobaciones y mostramos un .png con el mismo nombre que el estado, pero en minúsculas:

<img src="${request.contextPath}/images/${task.state}.png" alt="${task.state}"/>

Es estupendo refactorizar y dejarlo todo tan limpio y tan simple.

Muchas gracias a todos por los comentarios y por las ideas.

Print

12 respuestas a Clases de dominio con imágenes en Grails

Avatar

Alvaro Sánchez-Mariscal

1 de Octubre de 2009 a las 9:11

Buenas, un par de observaciones:

1) ¿Has probado ? Hablando de memoria diría que funciona, GSP es mucho más potente que JSP+EL.
2) Hasta donde yo sé, sólo tienes que marcar como transient propiedades, no métodos.

Un saludo.

Avatar

Twitted by alvaro_sanchez

1 de Octubre de 2009 a las 9:13

[...] This post was Twitted by alvaro_sanchez [...]

Avatar

Alvaro Sánchez-Mariscal

1 de Octubre de 2009 a las 9:33

Te lo pongo sin los tags:

g:if test=”${task.state == TaskState.INBOX}”

Avatar

Daniel Ortega

1 de Octubre de 2009 a las 9:44

Buena idea para solventar ese problema, voy a comentarte cómo lo hubiera hecho yo.
Primero he de dejar claro que yo soy Javero y apenas sé nada de groovy/grails.

La primera opción no te funciona (casi seguro) por comparar enums con cadenas.

Yo habría añadido un atributo de tipo String para cada valor del enumerado que coincidiera exactamente con el nombre del fichero de imagen que quieres mostrar, y un método para recuperarlo.

enum TastkState {
INBOX(”inbox.png’)
….

private String fileName;

private TastkState(String fileName) { this.fileName = fileName; }

public String getFileName(){..}
}

En la gsp tendrías algo como esto, sin ifs ni nada (te lo pongo en al viejo estilo que no sé nada de grails xD):

img src=”/imagenes/${task.fileName}” alt=”..:”

Yo he usado soluciones así varias veces, hacerlo o no depende de tu forma de pensar.
Hay gente que te diría que es una barbaridad el meter “harcoded” en el enum el nombre de las fotos (y, hasta cierto punto, llevan razón) pero, por otra parte, resuelves el problema rápidamente y con una línea de código :D

Avatar

Twitted by nacho_brito

1 de Octubre de 2009 a las 11:31

[...] This post was Twitted by nacho_brito [...]

Avatar

Tweets that mention Raúl Expósito | Clases de dominio con imágenes en Grails -- Topsy.com

1 de Octubre de 2009 a las 12:16

[...] This post was mentioned on Twitter by groovytweets and Fátima Casaú. Fátima Casaú said: RT @alvaro_sanchez: Clases de dominio con imágenes en #grails: http://bit.ly/4vdlxt [...]

Avatar

chechu

1 de Octubre de 2009 a las 13:37

Estoy con Daniel, una línea es mejor que dos, pero no hace falta meter el atributo con el nombre de la imagen en el enumerado. Puedes usar el propio nombre del enumerado:

En el directorio images tienes una imagen por cada estado con el nombre de ese estado (INBOX.png, etc.). Para el texto alternativo a la imagen, en el fichero de i18n tienes entradas como estas:

prefijo.INBOX = Inbox
prefijo.NEXT = Next

Al añadir nuevos estados no tendrás que tocar la gsp, sólo el enum, añadir la imagen en el directorio images y añadir una entrada en el fichero i18n.

Avatar

Twitted by fatimacasau

1 de Octubre de 2009 a las 14:41

[...] This post was Twitted by fatimacasau [...]

Avatar

Raúl Expósito

1 de Octubre de 2009 a las 14:45

Muchas gracias a todos por los comentarios

Álvaro, hice las pruebas y aunque gsp sea más potente la comprobación con el == siempre devuelve falso. Seguramente sea porque compara una cadena con un tipo enumerado.

Sobre lo de transient, en este caso lo que quiero es realizar un “cálculo” y sólo necesito el getter (en este caso el is). De hecho Hibernate protestaba porque no implementaba un setter, y si lo implementaba, porque no habia nada “detrás”. Quizá en este ejemplo tan sencillo no se vea bien, pero imaginate que queremos calcular el total de una factura que tiene muchos conceptos. En ese caso no tendremos un atributo transient ‘total’, sino solo un método getTotal() que debe ser transient y que cuando se invoque devuelva la suma de los conceptos.

Y a mi la idea de Daniel también me parece buena, solo que quitaría lo del ‘.png’ y eso lo dejaría ya dentro del gsp

img src=”/imagenes/${task.name}.png” alt=”..:”

Además esa idea también ayudaría en los properties con las traducciones como dice Chechu

Voy a darle una vuelta porque efectivamente una línea es mejor que dos.

Y de nuevo muchas gracias a todos por los comentarios :)

Avatar

Raúl Expósito

1 de Octubre de 2009 a las 20:36

Daniel, diste en el clavo. He editado el final de la entrada para, partiendo de tu idea, reducir el código lo máximo posible sin cablear los estados con los nombres de las imágenes.

Ahora si que ha quedado fino y elegante, elegante a la par que sencillo, o la frase que más os guste :)

Saludos

Avatar

ALbert

10 de Enero de 2012 a las 19:37

Muy buen ejemplo, gracias a las personas que se toman tiempo en todo esto

Avatar

Raúl Expósito

10 de Enero de 2012 a las 20:13

Las gracias hay que dártelas a ti por tomarte las molestias en dejar este amable comentario.

Gracias de nuevo y saludos

Deja tu comentario


subir