Skip to main content

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

Comments

Popular posts from this blog

Installing and Configuring NextPVR as a Replacement for Windows Media Center

If you follow me on Google+ you'll know I had a recent rant about Windows Media Center, which after running fine for about a year suddenly decided as of January 29 it was done downloading the program guide and by extension was therefore done recording any TV shows.

I'll spare you more ranting and simply say that none of the suggestions I got (which I appreciate!) worked, and rather than spending more time figuring out why, I decided to try something different.

NextPVR is an awesome free (as in beer, not as in freedom unfortunately ...) PVR application for Windows that with a little bit of tweaking handily replaced Windows Media Center. It can even download guide data, which is apparently something WMC no longer feels like doing.

Background I wound up going down this road in a rather circuitous way. My initial goal for the weekend project was to get Raspbmc running on one of my Raspberry Pis. The latest version of XBMC has PVR functionality so I was anxious to try that out as a …

Setting Up Django On a Raspberry Pi

This past weekend I finally got a chance to set up one of my two Raspberry Pis to use as a Django server so I thought I'd share the steps I went through both to save someone else attempting to do this some time as well as get any feedback in case there are different/better ways to do any of this.

I'm running this from my house (URL forthcoming once I get the real Django app finalized and put on the Raspberry Pi) using dyndns.org. I don't cover that aspect of things in this post but I'm happy to write that up as well if people are interested.

General Comments and Assumptions

Using latest Raspbian “wheezy” distro as of 1/19/2013 (http://www.raspberrypi.org/downloads)We’lll be using Nginx (http://nginx.org) as the web server/proxy and Gunicorn (http://gunicorn.org) as the WSGI serverI used http://www.apreche.net/complete-single-server-django-stack-tutorial/ heavily as I was creating this, so many thanks to the author of that tutorial. If you’re looking for more details on …

The Definitive Guide to CouchDB Authentication and Security

With a bold title like that I suppose I should clarify a bit. I finally got frustrated enough with all the disparate and seemingly incomplete information on this topic to want to gather everything I know about this topic into a single place, both so I have it for my own reference but also in the hopes that it will help others.Since CouchDB is just an HTTP resource and can be secured at that level along the same lines as you'd secure any HTTP resource, I should also point out that I will not be covering things like putting a proxy in front of CouchDB, using SSL with CouchDB, or anything along those lines. This post is strictly limited to how authentication and security work within CouchDB itself.CouchDB security is powerful and granular but frankly it's also a bit quirky and counterintuitive. What I'm outlining here is my understanding of all of this after taking several runs at it, reading everything I could find on the Internet (yes, the whole Internet!), and a great deal…