Web flows w grails

Znowu lektura książki Definitive guide to grails przyśpieszyła. Tym razem bardzo długi rozdział, który przeczytałem bardzo szybko, o web flows w grails. Nie miałem wcześniej praktycznego do czynienia z przepływami (obieg i przepływ będę używał jako tłumaczenia flow). Wiedza jaką wyniosłem z tego rozdziału jest ogromna i żałuję, że wcześniej nie wziąłem się za grails. Jak Jacek Laskowski wielokrotnie wspominał nauka grails to także nauka wielu innych narzędzi i technologi. Ale do rzeczy.

Grails zapewnia wsparcie dla web flows opartego na spring web flow. Przepływy w grails są to serie stanów od rozpoczynające się od stanu początkowego a kończące na ostatnim, kolejno po sobie następujące. Niemożliwe jest wywołanie stanów nie po kolei. Spring Web Flow jest zaawansowaną maszyną stanów, flowExecutionKey i Id zdarzenia są przekazywane pomiędzy klientem a serwerem jako parametry w requescie, umożliwiając przejścia pomiędzy kolejnymi stanami.

W przeciwieństwie do Spring Web Flow, w grails nie jest wymagana żadna konfiguracja xml. Aby utworzyć obieg należy w kontrolerze utworzyć metodę, która kończy się na Flow (konwencja)

def shoppingCartFlow = {
...
}

Każdy przepływ ma swoje własne id, które ma nazwę taką jak ta metoda tylko, że bez słowa Flow (konwencja ponownie), czyli dla powyższego to będzie shoppingCart.

W przeciwieństwie do zwykłych akcji, akcja przepływu w ciele domknięcia nie definiują logiki a sekwencję stanów. Początkowym stanem, jest zawsze pierwszy stan zdefiniowany w przepływie.

def shoppingCartFlow = {
  showCart {
    on("checkout").to "enterPersonalDetails"
    on("continueShopping").to "displayCatalogue"
  }
...
}

Jeśli nastąpi zdarzenie checkout to wtedy przejdzie się do stanu enterPersonalDetails

Widok dla danego stanu znajduje się w grails-app/views/controllerName/flowId/state.gsp np
grails-app/views/store/shoppingCart/showCart.gsp

Stan końcowy to stan który nie przyjmuje parametrów lub jeden (w którym następuje przekierowanie do akcji lub innego obiegu).

Można zmienić nazwę widoku poprzez metodę render w stanie

showCart {
  render(view:"basket")
...
}

Istnieją stany akcji i stany widoku.
Stan widoku wstrzymuje wykonywanie obiegu w celu wyrenderowaniu widoku i interakcji z użytkownikiem. (np showCart wyżej ten z render jak i z on).
Stan akcji wykonuje blok kodu po, którym następuje przejście dalej. np

listAlbums {
  action {
    [ albumList:Album.list(max:10,sort:'dateCreated', order:'desc') ]
  }
  on("success").to "showCatalogue"
  on(Exception).to "handleError"
}

Obiegi posiadają swoje własne zasięgi i obiekty w nich muszą implementować java.io.Serializable:

  • flash – przechowuje obiekty dla obecnego i następnego żądania.
  • flow – przez cały czas przepływu, usuwając obieg kiedy zostanie osiągnięty ostatni stan.
  • conversation – dla danego obiegu i jego podobiegów.

Istnieją dwie opcje wywołania stanu z linku i z wysłania formy (submitt).

Z linka, gdzie action to id przepływu

<g:link controller="store" action="shoppingCart">My Cart</g:link>

tutaj zawsze zostanie utworzony nowy przepływ, a jeśli chcemy wywołać jakieś zdarzenie to zostaje dodany atrybut event

<g:link controller="store" action="shoppingCart" event="checkout">Checkout</g:link>

Użycie zdarzenia w formie wygląda trochę inaczej, zdarzenie jest w atrybucie name submitButton

<g:form name="shoppingForm" url="[controller:'store', action:'shoppingCart']">
...
  <g:submitButton name="checkout" value="Checkout" />
  <g:submitButton name="continueShopping" value="Continue Shopping" />
</g:form>

Aby zwalidować formularz w przepływie, to można przekazać go do stanu akcji. Jednak zalecane jest wywołanie akcji przejścia (transition action). Jeśli transition action się nie powiedzie to przejście jest zatrzymywane a stan przywracany.

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

Aby utworzyć podobieg należy użyć subflow(nazwaObieguFlow)

wrappingOptions {
  subflow(chooseGiftWrapFlow)
  on('giftWrapChosen') {
    flow.giftWrap = conversation.giftWrap
  }
  on('cancelGiftWrap'). to 'enterShippingAddress'
}

Zdarzeniami tutaj są wszystkie końcowe stany przepływu chooseGiftWrapFlow.

Istnieje w requescie właściwość xhr która określa czy żądanie jest ajaxowe.

W przepływach można zastosować CommandObject.

W obiegu można dynamicznie tworzyć przejścia pomiędzy stanami:

on('back').to {
  def view
  if(flow.genreRecommendations || flow.userRecomendations)
    view = "showRecommendations"
  else if(flow.lastAlbum.shippingAddress) {
    view = 'enterShipping'
  }
  else {
    view = 'requireHardCopy'
  }
return view
}

W różny sposób można zapisać przejścia między stanami

on('back').to 'enterShipping' // static String name
on('back').to { 'enterShipping' } // Groovy optional return
on('back').to { return 'enterShipping' } // Groovy explicit return

wszystkie powyżej są równoważne.

Poprzez assert można sprawdzić czy stan jest w odpowiednim stanie (masło maślane – ale chodzi o to czy nie pojawił się jakiś błąd np przy zapisie do bazy, pola są zwalidowane itp).

Testowanie przepływów jest wykonywane za pomocą klasy grails.test.WebFlowTestCase i są to testy integracyjne. Przy wygenerowaniu testu należy pamiętać o podmianie klasy z której nasza klasa dziedziczy (z GroovyTestCase na WebFlowTestCase).
Następnie należy zaimplementować abstrakcyjną metodę getFlow, która zwraca domknięcie, które reprezentuje web flow np

def controller = new StoreController()
def getFlow() {
  controller.buyFlow
}
%d blogerów lubi to: