iberck
10/26/2016 - 1:45 PM

Grails web flow

Grails web flow

Web Flow

  • No utiliza la sesión, utiliza formularios serializados y el request.
  • Modela una máquina de estados
  • Agrega los scopes flow y conversation
  • Soluciona el problema del botón back (lanza una petición al presionar el back del browser)
  • Evita double submits.
  • Evita mostar las variables en la URL.

Start/End states

start state: Por convensión siempre será el primer bloque definido en el flow

end state: Por convensión se identifica de 2 maneras:

state {
  // redirecciona a otra acción y TERMINA EL FLOW:
  redirect(controller:"X", action:"Y")
}
state() // renderiza `state.gsp` Y TERMINA EL FLOW:

Una vez que un flow ha terminado, este solo puede ser reanudado desde el estado inicial.

View state

Es un estado que renderiza una vista

class TestController {

  def cartFlow = {
    viewState { // por convensión renderiza "/test/cart/viewState.gsp"
      on("event").to "state"
      on("event").to "state"
    }   
  }
}

Se puede cambiar la convensión, sin embargo solo se puede renderizar una vista (por ejemplo no acepta renderizar un template o un texto):

class TestController {

  def cartFlow = {
    viewState {
      render(view: "/test/wizard/viewStateAdvanced")
      on("event").to "state"
      on("event").to "state"
    }   
  }
}

Action state

Un action state es un estado que ejecuta código pero no renderiza una vista. Una vez ejecutada la acción, el modelo retornado se sube por automático al flow scope:

Una vez ejecutada la acción, grails lanza por automático los siguientes eventos:

success: se lanza cuando se ejecuta correctamente la acción.

Exception: se lanza cuando ocurre un error dentro de la acción.

class TestController {

  def cartFlow = {
    actionState {
      action {
        // return model (TO FLOW SCOPE)
      }
      on("success").to "state1"
      on(Exception).to "state2"
    }   
  }
}

Lanzar eventos personalizados

Nota: Solo aplica para action states

shippingNeeded {
   action {
       if (params.shippingRequired) yes()
       else no()
   }
   on("yes").to "enterShipping"
   on("no").to "enterPayment"
}

Transition actions

Son acciones que se ejecutan luego de lanzado un evento en un view state. Este tipo de acciones se utilizan para hacer databinding/validation.

enterPersonalDetails {
   on("submit") {Person person->
      flow.person = person
      !flow.person.validate() ? error() : success()
   }.to "enterShipping"
   on("return").to "showCart"
}

Note que el anterior ejemplo se trata de un view state. Un transition action no tiene sentido dentro de un action state ya que no se podría realizar databinding/validation porque la acción no tendría una vista asociada.

Por convensión se pueden lanzar los eventos:

error(): retorna a la misma página del estado para poder corregir el error.

success(): indica que se ejecutó exitosamente la transition action y envía al estado definido en to (en este caso a "enterShipping").

Flow scopes

request: No se puede utilizar para enviar datos de un estado a otro ya que en cada nuevo estado se crea un request.

flash: Web flow define su propio flash scope, aunque tiene la misma semántica que el flash scope de grails, es un scope diferente. Esto significa que los objetos guardados en el flash scope de web flow no serán visibles en las acciones standard de grails.

Los objetos depositados en el flash scope vivirán en este estado y en el siguiente.

Al realizar la transición entre estados mueve los objetos del flash hacia el request de tal manera que los valores se acceden como: ${message} ó ${request.message}.

Por una extraña razón (bug) cuando se pasa un command object (y seguramente también un domain class) a través del flash scope, sí pasa correctamente el objeto pero limpia su propiedad errors. Por lo tanto no se recomienda utilizar el flash scope mas que para pasar mensajes de texto.

flow: Los objetos viven durante todo el flow (no está presente en los subflows) y se borran automáticamente al llegar al fin del flow.

conversation: Los objetos viven durante todo el flow y sus subflows, los objetos se borran automáticamente al llegar al fin del flow.

Web flow hace un merge de los objetos del scope request/flash/flow/conversation y los mueve hacia el modelo de la vista de tal forma que se puedan accesar de la siguiente manera:

Controlador: flow.card="algo", vista: ${card}.

Serialización

Todo objeto (incluidos los command objects) introducido al scope flash/flow/conversation debe implementar Serializable, de lo contrario lanzará errores impredecibles. Cuando un objeto NO implementa Serializable, no es cargado correctamente por webflow por lo que todos sus atributos vienen null y el framework lanza errores "extraños" indicando que los atributos del objeto son null.

Lo mismo aplica para las asociaciones y los closures de los objetos los cuales se pueden marcar como transient o convertirlos en métodos.

El objeto flow contiene una referencia a la sesión de hibernate. Por lo tanto cualquier objeto leido dentro de la sesión de hibernate a través de un query GORM estará dentro del objeto flow y deberá implementar Serializable.

Se observó que los objetos utilizados en una acción del webflow también necesitan implementar Serializable aunque no sean subidos al flow scope. Esto es porque dichos objetos son subidos implicitamente a través de la sesión de hibernate.

Si no quiere marcar su clase como Serializable o guardarla en el flow, deberá hacer un evict de la entidad antes del fin del estado: flow.persistenceContext.evict(it)

Otro problema que se da cuando un objeto no está serializado es que lanza NPE cuando se intentan hacer queries en los estados del webflow. Esto lo experimenté cuando utilizaba en una gsp Conekta.setPublicKey('${Params.instance.currentPublicKey}'); sin embargo Params no implementaba Serializable.

Se creó el método GrailsUtils.inspectSerializableFlow(flow) el cual busca los objetos que se encuentran en el flow y no implementan `Serializable``. En este momento el método no realiza búsquedas recursivas.

Serialización e inyección de servicios

Cuando un servicio no utiliza transacciones (está marcado como static transactional = false), podrá ser inyectado dentro de un domain class marcándolo como transient de la siguiente manera:

class ConceptoPago implements Serializable {
    transient TipoCambioService tipoCambioService
    static transients = ['tipoCambioService']
}

transient indica que el servicio no será serializado, static transients indica que la propiedad NO será mapeada hacia la base de datos.

Si el servicio no es transaccional, también se puede crear una clase Helper e inyectarla como un spring bean al domain class. La clase tendrá que implementar Serializable o ser marcada como transient con el método antes descrito.

Si el servicio UTILIZA transacciones, no es posible inyectarlo dentro del domain class ni siquiera marcándolo como transient ya que la anotación @Transactional no implementa Serializable. La solución consiste en no inyectar el servicio en el domain class. Para utilizar el servicio dentro del domain class no hay que inyectarlo sino obtener el bean dentro del método que lo vaya a utilizar. Esta metodología también funciona para los servicios no transaccionales por lo que es la técnica recomendada:

Integer getPuntosDisponiblesAnio() {
    // Por una extraña razón no funciona con un método de utilería, es necesario utilizar
    // directamente la clase Holders
    PuntosService puntosService = Holders.applicationContext.getBean("puntosService")
}

Otra solución para inyectar servicios dentro de los domain class es haciendo que implementen Serializable, sin embargo no es la solución más intuitiva porque si utiliza a otros servicios, dichos servicios también deberán implementar Serializable.

Databinding y validación

Por alguna razón no funciona el constructor del domain/command con el objeto params en grails 2.5.2, por lo tanto el siguiente código recomiendado en la documentación oficial no funciona:

showPerson {
    on("submit"){
        flow.person = new PersonCommand(params)
        !flow.person.validate() ? error() : success()
    }.to "showCart"
    on("return").to "showCart"
}

Otra manera correcta es pasando el Command object como argumento del transition action, tal como sucede en las acciones, grails hace automáticamente el databinding del command object y enseguida ejecuta el método validate(), sin embargo esta alternativa no funciona en la versión 2.5.2 con los objetos Date (y tal vez con otros):

showPerson {
    on("submit"){PersonCommand person ->
        flow.person = person
        !flow.person.validate() ? error() : success()
    }.to "showCart"
    on("return").to "showCart"
}

Una alternativa correcta es construir el command object a partir de los params con bindData:

showPerson {
    on("submit"){
        def person = new PersonCommand()
        bindData(person, params)
        flow.person = person
        !flow.person.validate() ? error() : success()
    }.to "showCart"
    on("return").to "showCart"
}

Subflows

Un subflow es un flow dentro de otro flow.

Utilizar el scope conversation para pasar una entrada/salida entre flows puede ser comparado con utilizar variables globales para pasar información entre métodos. En algunas circunstancias está bien, pero usualmente es mejor utilizar métodos con argumentos y un retorno, esto significa definir argumentos input output del flow.

def shopingCartFlow = {
   // ...
    extendedSearchState {
        // INVOCAR EL SUBFLOW
        subflow(controller: "test", action: "extendedSearch", input: [expertise: "generic programmer"])
        on("stopSearch") { // EVENTO LANZADO POR EL SUBFLOW
            // ACCEDER AL RESULTADO DEL SUBFLOW
            flow.profesion = currentEvent.attributes.profesion
        }.to "showCart"
    }
    // ...
}

def extendedSearchFlow = {
    // VARIABLES DE ENTRADA DEL FLOW (por defecto deposita las variables en el scope flow)
    input {
        expertise(required: true) // variable requerida
        title("Search person") // variable opcional con valor default
    }
    startSearch {
        on("stop").to "stopSearch"
    }
    stopSearch {
        // VARIABLES DE SALIDA DEL FLOW
        output {
            profesion {
                "JVM programmer"
            }
        }
    }
}

State callbacks

onEntry

Código que se ejecuta cuando se entra al state desde otro state. Por ejemplo, los errores de validación (error event) no ejecutarán onEntry ya que se está ingresando al estado desde el mismo estado.

moreResults {
    onEntry {
    }
    on("back").to "startSearch"
}

onRender

Se ejecuta antes de renderizar la vista asociada al state. En pruebas también se ejecuta cuando se presiona el botón back.

moreResults {
    onRender() {
    }
    on("back").to "startSearch"
}

onExit

Se ejecuta cuando se sale del estado, es decir cuando se lanza hacia un evento que lanza a otro estado (si se regresa al mismo estado no lanza el callback).

¿Cómo enviar un modelo a una vista?

No es posible enviar un modelo a una vista tal como se hace en los controladores, en webflow hay que subir el objeto al flow scope. Otra manera es retornando un mapa en un action state, sin embargo lo que hace internamente es subir el mapa al flow scope.

Logging

Es necesario agregar la siguiente línea en Config.groovy para que se puedan mostrar los logs en nivel debug:

debug 'org.codehaus.groovy.grails.webflow'

Se encontró un bug en donde todo error de webflow lanzaba un StackOverflowError en las clases de log, dicho error se encontró porque había una , al final de las sentencias de log:

debug 'grails.app'
      'us.incorpora.sigrem',
      'org.hibernate.SQL', // LA "," HACE QUE GRAILS LANCE ERRORES IMPREDECIBLES POR LO QUE NO DEBE ESTAR.

Id en URL

Por una extraña razón cuando se envía un campo llamado id, lo pone en la url aunque el formulario se envíe por POST. Cuando el formulario tiene varios campos llamados id, en la url pone algo similar a: http://localhost:8080/sigrem/reserva/wizard/[Ljava.lang.String;@26111edc?execution=e9s3. La manera de solucionarlo es nombrando el campo id de otra manera (p.e. _id).

Botones back/next del browser

Al presionar el botón back del browser no ejecuta el código de la acción pasada, es decir luego de presionar back no ejecutaría el método onEntry de la acción anterior, tampoco el action (en caso de ser un action state).

La excepción que encontré es que si ejecuta el onEntry/action cuando termina el flow y presiono back.

Recursos

Diagrama de máquina de estados con su correspondiente código

Video Grails web flow

Presentación Grails webflow

<g:form>
  <g:submitButton name="EVENT" value="Enviar"/>
</g:form>

<g:form event="EVENT">
  <button type="submit">Enviar</button>
</g:form>
<g:link event="EVENT" params="[execution:flowExecutionKey]">Siguiente</g:link>

${createLink(event:"EVENT", params:[execution:flowExecutionKey])}