Wednesday, October 21, 2009

Session Notes - Spring WebFlow in Grails

Presenter: Joseph Nusairat, Integrallis About This Talk

  • discuss web flows
  • what are spring web flows
  • show some basics of webflow
  • live demo
  • Grails web flows didn't really mature until Grails 1.1.1

What is Web Flow

  • allows for controlling the flow of items via steps, substeps, other flows
  • use when you want to control the sequence in which events occur in a series of events
    • e.g. buying a plane ticket on travelocity--goes through a specific series of steps
    • can't go to point D without going through A, B, and C first
    • also support branches
  • control is not on the page but in a DSL
  • can keep components open for a longer period of time
  • where do you store the information for something like an uncompleted plane ticket order?
    • can store in the database, but would require cleanup for things that never make it to checkout
    • general way people have done this is via session data
      • check out spring memory management session recording on infoq
      • don't want to use up a lot of your server RAM with sessions that are unused
  • at the end of the flow, data is persisted to a database or somewhere more permanent
  • when you remove the flow aspect of things from the pages themselves, it makes things more flexible and configurable
  • SEAM allows for different timeouts for conversations and sessions themselves

WebFlows in Spring

  • was new in Spring 2.0
  • great integration with JSF for rich UI
  • allows control over the scoping of objects for a limited period of time
  • cannot have multiple conversations
  • uses xml
    • xml is great for transferring objects, but not great for configuration
    • configurations aren't generally that black and white

Creating a WebFlow

  • WebFlow in 1.2 M3 doesn't work at the moment
  • GRAILS-5185
  • demo will be in 1.1.1
  • in 1.2 WebFlow is a plugin (grails install-plugin webflow)
    • in 1.1.1 it's built in, but in 1.2 you have to install the plugin to use it
  • flows live in the controller
    • look just like an action, except ends with "Flow"
      • e.g. def bookingCartFlow = {}

Defining a View

  • views represent the individual pages
  • demo of hotel booking flow
    • "do you want to pay via paypal?" -- decision point with branch
  • def bookingCartFlow = {
    start() {}
    }
    • whatever is defined first is the starting point--doesn't have to be called start
  • DSL used to direct the flow, e.g. on("submit").to "end"

Domain Model

  • don't have to worry too much about the domain model except for the object that's used to track the conversation
  • Booking class is what's used to track the conversation and isn't persisted until the end
    • must implement Serializable in order to work

Creating a Controller and Actions

  • create a controller as per usual, but create an action ending with "Flow"
  • def bookingCartFlow = {
    start() {
    on("next") {
    // do work here
    log.info "inside start"
    }.to "stepA"
    on("done").to "end"
    }

    stepA() {
    on("prev").to "start"
    on("done").to "end"
    }

    end() {
    // define action state
    action {
    log.info "finish what you're done with"
    }
    }
    }

  • make sure to follow the DSL--doesn't necessarily error out if you aren't following the DSL
  • examples here are all done in the controller but in a real app you'd be using a service layer
    • according to the docs transactional needs to be set to false, but it doesn't work this way currently (1.1.1)
    • can do with transaction inside services if you're trying to save states in a particular order

Views

  • in flows you can have subfolders below the view directory for the controller
  • class HotelController {
    def index = { redirect(action:'bookingCart') } // this will go to the bookingCartFlow

    def paypalFlow = {
    pay() {
    on("submit") {
    log.info "Was it paid? ${params.paid}"
    }.to "evaluatePayment"
    }

    evaluatePayment() {
    action {
    if (params.paid == 'yes') {
    conversation.booking.paid = true
    return paidInFull
    } else {
    return paymentDeclined()
    }
    }
    }
    }

    def bookingCartFlow = {
    start() {
    action {
    log.info = "-start the booking-"
    }
    on("success").to "findHotels"
    }

    findHotels() {
    on("showHotels") {
    // find the hotel
    def hotels = Hotel.findAllByNameILike("%${params.hotelName}%", [max:10])
    [hotelResults : hotels]
    }.to "hotelListPage"
    }

    hotelListPage() {
    on("view") {
    flow.hotel = Hotel.get(params.id)
    }.to "hotelView"

    on("back") {
    }.to "findHotels"
    }

    hotelView() {
    on("back").to "start"
    on("book").to "bookingPage"
    }

    bookingPage() {
    on("proceed") {
    def booking = new Booking(params)
    booking.hotel = flow.hotel
    conversation.booking = booking
    if (!booking.validate()) {
    return error() // returns back to the page they came from and outputs errors
    }
    }.to "saveBooking"
    }

    saveBooking() { // the parens here are optional
    // no corresponding view to "saveBooking" so this is a decision node
    action {
    if (params.paymentType == "creditCard") {
    return creditCard()
    } else {
    return paypal()
    }
    }
    on("creditCard").to "creditCard"
    on("paypal").to "paypal"
    }

    paypal() {
    subflow(paypalFlow)
    on("paidInFull")
    }

    creditCard() {}
    }
    }

  • to identify an event in a flow, you can use the name of a g:button as the event you want to trigger when the button is clicked
  • to use an event in a g:link, add event parameter to g:link, e.g. <g:link event="view" ... />

Scopes Available in Flows

  • request
  • params
  • session
  • flash
  • flow
    • lasts for the duration of the flow
    • removed when flow ends
  • conversation
    • lasts for the length of the flow and nested subflows
    • subflows are good for things like different payment methods--can be reused across multiple flows
  • best practice point: don't refer to anything about flows or conversations in gsps
    • this ties the view to a specific flow or conversation and reduces the ability to reconfigure and reuse things

What are subflows

  • ability to create alternative flow paths
  • call subflows via flows
  • are their own flows themselves
  • name with "Flow" at the end
  • calling a subflow
    • subflow(subflowName)

Can you call to flows in different controllers?

  • yes, but it gets a bit funky
  • define flow in another controller as per usual, but declare the flow as static
  • in other controller, do def creditcardFlow = BillingController.creditcardFlow
  • still have to put the billing views under the hotel views as opposed to the billing views

How do you end the flow?

  • if a flow doesn't do a transition or a redirect, the flow ends

Final thoughts

  • don't call services with transactions--can cause unexpected problems
  • in grails 1.2 flows are a plugin
  • webflow moves objects from flash to request scope between transition states
  • don't include the scope prefix when calling on the page
    • merge flow/conversation to local scopes

http://github.com/integrallis/grails_web_flow_demo

No comments: