Grails webflow


 Ivo Houbrechts
       Ixor
About Ixor
•   Since 2002
•   40 experienced ICT professionals
•   Software development for the JVM
•   Consultancy and product development
•   Loyal customers in Finance, Telecom and Government




•   www.ixor.be
Agenda
• Grails webflow plugin
   • Demo app
   • Spring webflow framework
   • Flow states
   • Subflows
   • Scopes
   • Ajax
   • Testing
   • Tips & pitfalls
   • Alternatives
• Extended validation plugin
• Questions
Demo app
• Simple domain model:
    • Project
    • Developer
    • UserStory


• Wizard for creating a project
Spring webflow framework
• Manage page flows
  • Wizards
• Manage objects
  • Scopes
• Manage states
• Back-button hell
• Grails webflow plugin
  • Subset of features
Defining a grails webflow
def newProjectWizardCleanedUpFlow = {
        onStart doOnStart

       projectInfo {
           on("next", doBindProjectInfo).to("lead")
       }

       lead {
           subflow(controller: "developer", action: "getDeveloper",
                   input: [experience: Experience.SENIOR])
           on("selected", doSetLead).to(selectLeadOrTeam)
           on("cancel", doFlashLeadMandatoryMessage).to("lead")
       }

       team {
           on("add").to("addTeamMember")
           on("remove", doRemoveTeamMember).to("team")
           on("next").to("stories")
       }

       addTeamMember {
           subflow(controller: "developer", action: "getDeveloper”)
       …
Flow states
• View states
• Action states
• Start and end states
• Subflow states
View state: definition
• Renders a view
• Definition:
    projectInfo {
        on("next").to("lead")
    }




• Default view
   /controllerName/flowName/viewstateId


• Override with
   render(view: "myview")
View state: events
• Triggered by user
  • Link
     <g:link action="newProjectWizard” event="remove">Remove</g:link>


  • Button
    <g:form action="newProjectWizard">
        <g:submitButton name="add" value="Add developer"/>
    </g:form>



• Transition
    team {
        on("add").to("addTeamMember")
        on("remove").to("team")
    }
View state: transition actions
• Used for binding and validation
   on("next") {
       flow.projectInstance.properties = params
       if (!flow.projectInstance.validate(["name", "description"])) {
           error()
       }
   }.to("lead")

  • Supports command objects
• Dynamic transitions
   on("selected“).to {
       if (!flow.projectInstance.validate(["lead"])) {
           return "lead"
       } else {
           return "team"
       }
   }
Action state: definition
saveProject {
    action {
        if (flow.projectInstance.save()) {
             end()
        } else {
             projectInfo()
        }
    }
    on("end").to("end")
    on("projectInfo").to("projectInfo")
}
Action state: events
• Events
  • Explicit
     • call missing method
  • Implicit
     • success
     • exception
  saveProject {
      action {
          def project= projectService.save(flow.projectInstance)
          return [project: project]
      }
      on("success").to("end")
      on("ValidationException").to("retry")
      on("Exception").to("fatalError")
  }
Action state: returning a model

  saveProject {
      action {
          def project= projectService.save(flow.projectInstance)
          return [project: project]
      }
      on("success").to("end")
      on("ValidationException").to("retry")
      on("Exception").to("fatalError")
  }

• Merged into flow scope
• Used in next state or view
Start and end states
• Start state: first state in flow definition
• End state
  • Empty
       cancel()

  • Redirect
     end {
         redirect(action: "show", id: flow.projectInstance.id)
     }
Subflow states
• Like method calls
  • Reusable (parts of) flows
• Definition
    lead {
        subflow(controller: "developer", action: "getDeveloper",
                        input: [experience: Experience.SENIOR])
            on("selected“).to("team")
            on("cancel").to("lead")
    }


• Events
  • Last state of subflow
Subflow input (grails 1.4.0)
• Declare input arguments
  • Like method arguments (contract)
   def getDeveloperFlow = {
       input {
           experience()
           title(required: true)
       }
   …


• Provide values in subflow call
   subflow(controller: "developer", action: "getDeveloper",
          input: [experience: Experience.SENIOR, title: "Select lead"])
Subflow input (grails 1.4.0)
• Required arguments
  • Exception if not provided
  • Not possible to use flow standalone
• Default values
   def mySubFlow = {
       input {
           foo() // optional input with no default value
           bazz(required: false, value: someConstantValue)
           dynamic { flow.someProperty }
           dynamicBis (value: someNamedClosure)
       }
   …
Subflow output (grails 1.4.0)
• Define output in end state
  selected {
      output {
          developer {flow.developer}
      }
  }…




• Retrieve output in calling flow
  on("selected") {
      flow.projectInstance.lead = currentEvent.attributes.developer
  }.to(…)
Subflow input and output values
• Constant values
  • Defined at flow definition time (application startup)
  • Refer to (controller) variables or literal expressions
• Dynamic values
  • Evaluated at flow execution time
  • Defined with closures
• Applicable in:
  • Default input values
  • Input values in subflow call
  • Output values
Scopes
• Standard scopes
• Webflow scopes
  • flow
     • flow execution, only visible in one flow
     • serialized
  • conversation
     • flow execution, visible for all flows and subflows
     • Cf. class members ↔ method arguments (subflow input)
     • serialized
  • flash
     • semantics like grails flash, but different instance
     • merged with request scope → omit ‘flash.’ prefix
Ajax
• Define transition to current state
  • Render template in transition action
   stories {
       on("remove") {
           …
           render(template: "newProjectWizard/editStories",
                      model: [projectInstance: flow.projectInstance])
       }.to("stories")
   …

• Use remoteLink or formRemote
   <g:remoteLink update="editStories" controller="project“
         action="newProjectWizard" event="remove"
         params="${[name: userStory.name,
                    ajaxSource: true, execution: params.execution]}">
   Remove</g:remoteLink>
Testing
• Integration test flows
  class ProjectControllerTests extends WebFlowTestCase {

      @Override Object getFlow() {
          return controller.newProjectWizardFlow
      }

      protected void setUp() {
          //register all subflows
          registerFlow("developer/getDeveloper",
            new DeveloperController().getDeveloperFlow)
      }

      void testHappyFlow() {
          startFlow()
          assert "projectInfo" == flowExecution.activeSession.state.id
          controller.params.name = "project name“
          signalEvent("next")
          assert "x" == flowScope.projectInstance.lead.name
  …
Tip 1
• Returning flow output to other controller actions
  • Use standard grails flash scope
   RequestContextHolder.currentRequestAttributes().flashScope.message =
      "Project was successfully created"
Tip 2
• Create clean readable flow definitions
     • Use closure variables
 def newProjectWizardCleanedUpFlow = {
         onStart doOnStart

         projectInfo {
             on("next", doBindProjectInfo).to("lead")
         }
 …



     • Define private closures
 private def doBindProjectInfo = {
     …
 }
Tip 3
• Flow diagrams (intellij)
  • Demo app: grails generate-xml-flow-definitions
Tip 4
• Kick start flow views
  • grails generate-views
  • Change
     • action attribute in forms and links
     • explicitly fill in the controller attribute in forms and links when
       using subflows
     • add an event attribute to links
     • remove the flash prefix in flash.message
     • use submitButton in stead of actionSubmit
Tip 5
• Bread crumbs
  • <flow:breadCrumbs/>
  • Tag source code: see demo app
Pitfalls
• Implement java.io.Serializable
  • Objects in flow and conversation scope
  • Events don’t get fired
        • Missing methods are not missing (f.e. scaffold actions)
  • Limited execution history
     • Back navigation
     • Ajax calls increase state count
  • Last state is subflow state
     • Back navigation after flow completes redirects to subflow
Alternatives
• Ad hoc flows
  • Where to store state?
  • What if user quits in the middle and starts again?
• One-page wizards with ajax
  • One page
  • One controller action for each step
  • Where to store state?
  • Subflows with overlays
Extended validation plugin
• Validation for non-grails domain classes
  • Particularly useful in combination with webflow
• Cascade constraint
   static constraints = {
       customer(cascade: true)
   …


• Instance constraint
   static constraints = {
       creditCheck(validator: {order ->
           if (order.customer.creditScore == CreditScore.BAD
               && order.orderLines.size() > 1) {
                   return "order.creditCheck.BAD"
           }
       })
   …
Extended validation plugin
• Constraint groups
   static constraints = {
       invoiceData {
           customer(cascade: true)
           contactPerson(nullable: false, blank: false)
       }
   …


• Partial validation
   order.validate(groups: ["invoiceData"])
   order.validate(
       includes: ["orderLines"],
       excludes: ["orderLines.deliveryAddress"])


• getAllErrorsRecursive() dynamic method
Livesnippets
• groovy & grails related documentation
  • Demo application
  • Code snippets
     • Directly linked to demo code
• Github:
  https://github.com/houbie/livesnippets
• Cloudfoundry:
   http://livesnippets.cloudfoundry.com
Questions



  ?

GR8Conf 2011: Grails Webflow

  • 1.
    Grails webflow IvoHoubrechts Ixor
  • 2.
    About Ixor • Since 2002 • 40 experienced ICT professionals • Software development for the JVM • Consultancy and product development • Loyal customers in Finance, Telecom and Government • www.ixor.be
  • 3.
    Agenda • Grails webflowplugin • Demo app • Spring webflow framework • Flow states • Subflows • Scopes • Ajax • Testing • Tips & pitfalls • Alternatives • Extended validation plugin • Questions
  • 4.
    Demo app • Simpledomain model: • Project • Developer • UserStory • Wizard for creating a project
  • 5.
    Spring webflow framework •Manage page flows • Wizards • Manage objects • Scopes • Manage states • Back-button hell • Grails webflow plugin • Subset of features
  • 6.
    Defining a grailswebflow def newProjectWizardCleanedUpFlow = { onStart doOnStart projectInfo { on("next", doBindProjectInfo).to("lead") } lead { subflow(controller: "developer", action: "getDeveloper", input: [experience: Experience.SENIOR]) on("selected", doSetLead).to(selectLeadOrTeam) on("cancel", doFlashLeadMandatoryMessage).to("lead") } team { on("add").to("addTeamMember") on("remove", doRemoveTeamMember).to("team") on("next").to("stories") } addTeamMember { subflow(controller: "developer", action: "getDeveloper”) …
  • 7.
    Flow states • Viewstates • Action states • Start and end states • Subflow states
  • 8.
    View state: definition •Renders a view • Definition: projectInfo { on("next").to("lead") } • Default view /controllerName/flowName/viewstateId • Override with render(view: "myview")
  • 9.
    View state: events •Triggered by user • Link <g:link action="newProjectWizard” event="remove">Remove</g:link> • Button <g:form action="newProjectWizard"> <g:submitButton name="add" value="Add developer"/> </g:form> • Transition team { on("add").to("addTeamMember") on("remove").to("team") }
  • 10.
    View state: transitionactions • Used for binding and validation on("next") { flow.projectInstance.properties = params if (!flow.projectInstance.validate(["name", "description"])) { error() } }.to("lead") • Supports command objects • Dynamic transitions on("selected“).to { if (!flow.projectInstance.validate(["lead"])) { return "lead" } else { return "team" } }
  • 11.
    Action state: definition saveProject{ action { if (flow.projectInstance.save()) { end() } else { projectInfo() } } on("end").to("end") on("projectInfo").to("projectInfo") }
  • 12.
    Action state: events •Events • Explicit • call missing method • Implicit • success • exception saveProject { action { def project= projectService.save(flow.projectInstance) return [project: project] } on("success").to("end") on("ValidationException").to("retry") on("Exception").to("fatalError") }
  • 13.
    Action state: returninga model saveProject { action { def project= projectService.save(flow.projectInstance) return [project: project] } on("success").to("end") on("ValidationException").to("retry") on("Exception").to("fatalError") } • Merged into flow scope • Used in next state or view
  • 14.
    Start and endstates • Start state: first state in flow definition • End state • Empty cancel() • Redirect end { redirect(action: "show", id: flow.projectInstance.id) }
  • 15.
    Subflow states • Likemethod calls • Reusable (parts of) flows • Definition lead { subflow(controller: "developer", action: "getDeveloper", input: [experience: Experience.SENIOR]) on("selected“).to("team") on("cancel").to("lead") } • Events • Last state of subflow
  • 16.
    Subflow input (grails1.4.0) • Declare input arguments • Like method arguments (contract) def getDeveloperFlow = { input { experience() title(required: true) } … • Provide values in subflow call subflow(controller: "developer", action: "getDeveloper", input: [experience: Experience.SENIOR, title: "Select lead"])
  • 17.
    Subflow input (grails1.4.0) • Required arguments • Exception if not provided • Not possible to use flow standalone • Default values def mySubFlow = { input { foo() // optional input with no default value bazz(required: false, value: someConstantValue) dynamic { flow.someProperty } dynamicBis (value: someNamedClosure) } …
  • 18.
    Subflow output (grails1.4.0) • Define output in end state selected { output { developer {flow.developer} } }… • Retrieve output in calling flow on("selected") { flow.projectInstance.lead = currentEvent.attributes.developer }.to(…)
  • 19.
    Subflow input andoutput values • Constant values • Defined at flow definition time (application startup) • Refer to (controller) variables or literal expressions • Dynamic values • Evaluated at flow execution time • Defined with closures • Applicable in: • Default input values • Input values in subflow call • Output values
  • 20.
    Scopes • Standard scopes •Webflow scopes • flow • flow execution, only visible in one flow • serialized • conversation • flow execution, visible for all flows and subflows • Cf. class members ↔ method arguments (subflow input) • serialized • flash • semantics like grails flash, but different instance • merged with request scope → omit ‘flash.’ prefix
  • 21.
    Ajax • Define transitionto current state • Render template in transition action stories { on("remove") { … render(template: "newProjectWizard/editStories", model: [projectInstance: flow.projectInstance]) }.to("stories") … • Use remoteLink or formRemote <g:remoteLink update="editStories" controller="project“ action="newProjectWizard" event="remove" params="${[name: userStory.name, ajaxSource: true, execution: params.execution]}"> Remove</g:remoteLink>
  • 22.
    Testing • Integration testflows class ProjectControllerTests extends WebFlowTestCase { @Override Object getFlow() { return controller.newProjectWizardFlow } protected void setUp() { //register all subflows registerFlow("developer/getDeveloper", new DeveloperController().getDeveloperFlow) } void testHappyFlow() { startFlow() assert "projectInfo" == flowExecution.activeSession.state.id controller.params.name = "project name“ signalEvent("next") assert "x" == flowScope.projectInstance.lead.name …
  • 23.
    Tip 1 • Returningflow output to other controller actions • Use standard grails flash scope RequestContextHolder.currentRequestAttributes().flashScope.message = "Project was successfully created"
  • 24.
    Tip 2 • Createclean readable flow definitions • Use closure variables def newProjectWizardCleanedUpFlow = { onStart doOnStart projectInfo { on("next", doBindProjectInfo).to("lead") } … • Define private closures private def doBindProjectInfo = { … }
  • 25.
    Tip 3 • Flowdiagrams (intellij) • Demo app: grails generate-xml-flow-definitions
  • 26.
    Tip 4 • Kickstart flow views • grails generate-views • Change • action attribute in forms and links • explicitly fill in the controller attribute in forms and links when using subflows • add an event attribute to links • remove the flash prefix in flash.message • use submitButton in stead of actionSubmit
  • 27.
    Tip 5 • Breadcrumbs • <flow:breadCrumbs/> • Tag source code: see demo app
  • 28.
    Pitfalls • Implement java.io.Serializable • Objects in flow and conversation scope • Events don’t get fired • Missing methods are not missing (f.e. scaffold actions) • Limited execution history • Back navigation • Ajax calls increase state count • Last state is subflow state • Back navigation after flow completes redirects to subflow
  • 29.
    Alternatives • Ad hocflows • Where to store state? • What if user quits in the middle and starts again? • One-page wizards with ajax • One page • One controller action for each step • Where to store state? • Subflows with overlays
  • 30.
    Extended validation plugin •Validation for non-grails domain classes • Particularly useful in combination with webflow • Cascade constraint static constraints = { customer(cascade: true) … • Instance constraint static constraints = { creditCheck(validator: {order -> if (order.customer.creditScore == CreditScore.BAD && order.orderLines.size() > 1) { return "order.creditCheck.BAD" } }) …
  • 31.
    Extended validation plugin •Constraint groups static constraints = { invoiceData { customer(cascade: true) contactPerson(nullable: false, blank: false) } … • Partial validation order.validate(groups: ["invoiceData"]) order.validate( includes: ["orderLines"], excludes: ["orderLines.deliveryAddress"]) • getAllErrorsRecursive() dynamic method
  • 32.
    Livesnippets • groovy &grails related documentation • Demo application • Code snippets • Directly linked to demo code • Github: https://github.com/houbie/livesnippets • Cloudfoundry: http://livesnippets.cloudfoundry.com
  • 33.