25 stycznia 2009

Tworzenie interfejsu użytkownika w Grails - rozdział 5 z "Beginning Groovy and Grails"

Teraz dopiero zaczęło się niezwykle interesująco. Dopiero poczułem klimaty Grails. Przyszła pora na dwa mocne rozdziały, z których pierwszy to rozdział 5. "Building the User Interface" z książki Beginning Groovy and Grails: From Novice to Professional. W rozdziale dowiadujemy się o GSP (Groovy Server Pages), znacznikach grailsowych, szablonach i "uzbrajaniu" ich w CSS. Jako przykład praktyczny przedstawianych cech Grails tworzy się interfejs użytkownika z nagłówkiem, stopką, bocznymi panelami, dzieli się centralną przestrzeń na faktyczną zawartość strony i prawy "dodatek" w postaci listy znajomych. Do tego dodaje się podstawową funkcjonalność obsługi procesu uwierzytelnienia. Wszystko na 60 stronach (tak, jest trochę zrzutów ekranu, ale ilość informacji jest i tak przytłaczająca). Interesujące są wycieczki w stronę innych technologii oraz pojawia się coraz więcej porównań z Ruby on Rails.

Grails korzysta z szablonów i układów do budowy stron.

Szablony (ang. template) są w katalogu grails-app/views i rozpoczynają się od podkreślnika (_), co jest konwencją nazewniczą Grails dla wyróżnienia szablonów. Zaleca się, aby umieścić zestaw szablonów dla danej klasy dziedzinowej w jej własnym katalogu "widokowym", np. grails-app/views/user dla klasy dziedzinowej User. Wspólne, bardziej ogólne szablony, powinny znajdować się w grails-app/views/common.

Układ (ang. laytout) to zestaw szablonów w odpowiednim ułożeniu/rozkładzie. Tworzymy układ, np. main.gsp i zapisujemy w domyślnym katalogu grails-app/views/layouts wraz z CSS - main.css w web-app/css.

Dodajemy szablon do układu z pomocą <g:render>, np. <g:render template="/common/footer" />. Kolejną konwencją Grails jest określenie szablonu bez podkreślnika oraz rozszerzenia .gsp. Upiększamy dołączony szablon przez "opakowanie" <div>, z którym związujemy odpowiedni styl.

W pierwszym przykładzie w książce spotykamy się z kilkoma nowymi konstrukcjami GSP - g:layoutTitle, ${createLinkTo}, g:layoutHead, g:javascript, g:layoutBody oraz wspomnianym g:render. Są jeszcze kolejne, a w jednym z nich konstrukcja g:if/g:else, aby "oprogramować" if-then-else do wprowadzenia większej dynamiki w stronach GSP. To jest właśnie ten element Grails, którego nie mogę zaakceptować, podobnie jak znaczniki JSP czy skrypty JSP w JavaServer Pages (JSP). Znaczniki są elementami programistycznymi, a jedynie przypominają swoją budową elementy HTML. Podstawowy mój zarzut względem jakichkolwiek znaczników, w tym i Grails, to brak możliwości podejrzenia wyglądu strony korzystając z edytorów HTML. Żaden ze znanych mi narzędzi HTML nie rozumie znaczników Grails, więc, podobnie jak w JSP, tworzenie stron HTML będzie niezwykle uciążliwie. Gdybym nie znał rozwiązania tego problemu, pewnie wstrzymałbym się z tymi uwagami, ale znajomość Apache Wicket już nie pozwala mi na to (chociaż i tam pojawiają się dedykowane znaczniki). W Wicket (oraz w nieznanych mi Tapestry i Echo) łączenie stron HTML z odpowiadającym im klasom javowym odbywa się przy pomocy dodatkowego i akceptowanego przez edytory (X)HTML atrybutu wicket:id. Oby dla GSP i jego specyficznych znaczników była alternatywna, tak aby nie trzeba było poszukiwać nowych, dedykowanych narzędzi edycyjnych dla osób od HTML w zespole projektowym (nie wspominając o tych od "bebechów", tj. programistów javowo-grailsowych).

Główna strona startowa w aplikacji grailsowej to web-app/index.gsp.

W międzyczasie pojawia się kolejny znacznik GSP - g:each, który jest znacznikiem przejścia dostępnych wartości (atrybut var) w liście (atrybut in) oraz konwencja związana z układami - określenie domyślnego układu przez <meta name="layout" content="main" />, która wskazuje na grails-app/views/layouts/main.gsp.

Biblioteka znaczników Grails przypomina JavaServer Pages Standard Tag Library (JSTL) oraz znaczniki Struts. Po chwili znajdujemy w książce przedstawienie dostępnych znaczników grailsowych, które otrzymują miano "Grails' strength". Ja nie podzielam tego zdania, po tym kiedy zobaczyłem ładniejsze architektonicznie rozwiązanie z Apache Wicket - programista javy zajmuje się modelem, a ekspert HTML widokiem bez konieczności łączenia tych światów jakimikolwiek wymyślnymi konstrukcjami. Prostota ponad wszystko. Czyżbym czegoś niedostrzegał, skoro Grails jako nowy szkielet webowy promuje znaczniki zamiast automagicznego łączenia obu światów ala Wicket.

Znaczniki logiczne: g:if, g:else oraz g:elseif - badamy wyrażenie warunkowe w atrybucie test i na tej podstawie wykonywany jest właściwy blok. Nie ma czego wyjaśniać.

Znaczniki iteracyjne - g:while, g:each, g:collect, g:findAll, g:grep.

Znacznik przypisania - set (nie wspomina się o identyfikatorze g?)

Znaczniki linkowania - g:link, g:createLink oraz g:createLinkTo (który był wcześniej prezentowany w postaci wyrażenia ${createLinkTo}).

Znaczniki ajaksowe - g:remoteField, g:remoteFunction, g:remoteLink, g:formRemote, g:javascript, g:submitToRemote.

Znaczniki formularzy - g:actionSubmit, g:actionSubmitImage, g:checkBox, g:currencySelect i wiele innych, których zadaniem jest umożliwienie dynamicznego tworzenia struktur "formularzowych" HTML, tj. wypełnianie ich dynamicznymi wartościami.

Bardzo skroma kategoria znaczników budowania interfejsu użytkownika jest reprezentowana przez g:richTextEditor. Więcej znaczników dostarczanych jest przez dedykowane wtyczki grailsowe.

Następnie bardzo liczna grupa znaczników tworzenia szablonów i układów stron, z której poznaliśmy już g:render oraz rodzinę g:layout*. Jest ich znacznie więcej.

Znaczniki kontroli poprawności (walidacji) zamyka prezentację dostępnych znaczników GSP. Tutaj znajdziemy g:eachError, g:hasErrors, g:message oraz g:fieldValue.

Na podstawie <g:link controller="user" action="login"> przedstawia się kolejną konwencję grailsową - wskazanie jakie elementy aplikacji grailsowej zostaną wywołane. W tym przypadku będzie to wykonanie akcji login na kontrolerze UserController, a ostateczna strona, na którą zostanie przekierowanie żądania - login.gsp - znajduje się w grails-app/views/user. W tym momencie spodziewałbym się przedstawienia elementów adresu URL, które wskazują na kontroler, akcję (metodę kontrolera) i przekazywane parametry. To jednak pojawia się dużo, dużo później (niestety).

Deklarujemy akcję (metodę) kontrolera konstrukcją
 def <nazwa-akcji> = {}
w kontolerze, np. dla akcji login byłoby to
 def login = {}
W ciele metody można wykonywać wszystkie możliwe czynności (wywołania metod) jakie dostępne są w Grails/Groovy/Javie, czy co tam sobie jeszcze wymyślimy. W książce pojawiają się dwa przykłady na obsługę akcji handleLogin oraz logout.

Zgodnie z dobrą praktyką w aplikacjach webowych wywołanie akcji kończy się przekierowaniem (ang. redirect) do właściwej strony GSP, która odpowiada nazwie akcji (co w naszym przypadku byłoby login.gsp w grails-app/views).

Sprawdzenie poprawności działania kodu, a w tym konkretnym przypadku aplikacji grailsowej ze stronami GSP, jest możliwe dzięki dwóm powszechnie dostępnym szkieletom testowania JUnit oraz Canoo. Obsługują one testowanie jednostkowe (JUnit), integracyjne (JUnit) i funkcjonalne (Canoo). Wspomina się o Six Sigma i Poka-yoke, które jest japońskim słowem i nazwą dla metody popełniania pomyłek tak oczywistych, że ostatecznie zapobiega się im, właśnie przez tworzenie testów. Nigdy nie słyszałem o tym Poka-yoke, więc potwierdza się reguła, że warto czytać książki ;-)

Za pomocą polecenia grails create-controller tworzymy kontroler oraz pusty test integracyjny w test/integration. Test integracyjny tworzymy poleceniem grails create-integration-test, natomiast test jednostkowy grails create-unit-test (w katalogu tests/unit). Ważną różnicą między testami integracyjnym i jednostkowym w Grails jest fakt, że w klasie testu jednostkowego niedostępne są dynamiczne metody save, delete oraz findBy*. W końcu testy jednostkowe testują działanie logiki w pojedyńczej klasie (jednostce kompilacji), a integracyjne ze wszystkim wokół (m.in. z bazą danych). Wspomina się w jednym zdaniu o metodach MockFor* oraz StubFor*, ale nic poza wymienieniem ich nazwy.

Na przykładzie testu integracyjnego dla UserController można przekonać się jak prosto pisze się je w Grails. Baza jest, metody dostępu do bazy danych również (bo to test integracyjny, a nie jednostkowy, więc Grails dostarcza je dynamicznie) i wystarczy jedynie przypisywać parametry żądania HTML z formularza - mapa params - wykonać akcję (wywołanie metody) i sprawdzić, czy wynik jej działania jest jak oczekiwano, co w przypadku UserController i akcji handleLogin będzie umieszczeniem obiektu user w sesji (ponownie mapa, ale tym razem o nazwie session).

Uruchamiamy testy poleceniem grails test-app, z opcjonalnym podaniem nazwy klasy testowej, aby zawęzić ich liczbę, np. grails test-app UserController (wykonają się metody rozpoczynające się test z klasy UserControllerTests). Wynik działania testów pojawi się na konsoli oraz w raporcie w test/reports.

W końcu przychodzi nam poznać wtyczkę grailsową Canoo WebTest, za pomocą której tworzymy testy funkcjonalne. Instalujemy wtyczkę poleceniem grails install-plugin webtest. Następnie poleceniem grails create-webtest <klasa-dziedzinowa> tworzymy test jednostkowy dla wybranej klasy dziedzinowej w katalogu webtest/tests/testsuite.groovy, która rozszerza grails.util.WebTest (z konfiguracją webtest/conf/webtest.properties). testsuite.groovy wykonuje metodę suite() dla wszystkich klas z katalogu webtest/tests zakończonych na Test. Wykonujemy testy poleceniem grails run-webtest. Wynik prezentowany jest w przeglądarce.

Wszystkie szkielety webowe udostępniają mechanizm "wyniesienia" ciagów znaków wrażliwych na ustawienia międzynarodowe do zewnętrznych plików komunikatów (ang. message bundles). Działanie opiera się na działaniu plików komunikatów w Javie. Domyślny plik komunikatów messages.properties znajduje się w katalogu grails-app/i18n (dla przypomnienia: i18n = internationalization, tj. i + 18 znaków + n, podobnie jak l10n = localization). Atrybut code w znaczniku g:message wskazuje na klucz w pliku komunikatów. Należy uważać na testy funkcjonalne z użyciem Canoo WebTest i jego metody verifyText - specjalnego wsparcia nie ma w związku ze zmianą ustawień międzynarodowych, więc samemu należy zadbać o zgodność testów i aplikacji.

Kolejna sekcja w tym rozdziale dotyczy kontroli poprawności wprowadzonych danych (walidacja). Wspominałem już w poprzednich relacjach, że w klasie dziedzinowej można określić warunki jakie musi spełniać klasa przed zapisem - static contraints. Ich niespełnienie spowoduje wyświetlenie strony z domyślnymi komunikatami na czerwono. Do ich zmiany musimy wygenerować statyczną wersję aplikacji w trybie scaffolding, tzw. static scaffolding poleceniem grails generate-views <klasa-dziedzinowa>. Grails utworzy strony create.gsp, edit.gsp, list.gsp i show.gsp w katalog grails-app/views/<klasa-dziedzinowa>. Następnie konieczne jest stworzenie "statycznego rusztowania" (ang. static scaffolding) dla kontrolera poleceniem grails generate-controller <klasa-dziedzinowa>. Uwaga na nadpisanie kontrolera dla danej klasy dziedzinowej. W stworzonych stronach GSP w ruch idą znaczniki g:hasErrors oraz g:renderErrors.

Autorzy przechodzą dalej do omówienia dodatkowego zasięgu (przestrzeni) widoczności obiektów - flash, który wypada pomiędzy żądaniem (request), a sesją (session). Jest on szczególnie przydatny w przypadku domyślnego przekierowywania żądania do strony GSP, przy którym parametry żądania giną, a sesyjne istnieją zbyt długo i należałoby pamiętać o ich "ręcznym" usunięciu. Poza flash mamy do dyspozycji zasięgi: application, session, request oraz page. Wszystkie są mapami. Istnieje możliwość zdefiniowania własnych przestrzeni. Tutaj pojawia się wzmianka o podobieństwie z Ruby on Rails, gdzie pojęcie takiej przestrzeni również istnieje. Łącząc g:message (wyświetlanie komunikatów z zewnętrznego pliku komunikatów) oraz przestrzeń flash, w którym umieszczamy klucze komunikatów możemy obsłużyć temat umiędzynarodowienia komunikatów błędów.

W końcu pojawia się omówienie tematu kontrolowania przebiegu w aplikacji z poziomu kontrolera (jako bardziej właściwego niż kopiowanie podobnej funkcjonalności w widokach), na przykładzie dostępu do danych użytkownika A innym użytkownikom. Jako przykład przytoczę Listing 5-30 z książki:
 def edit = {
if (session.user.id != params.id) {
flash.message = "You can only edit yourself"
redirect(action:list)
return
}
}
Wyjaśnienie działania akcji edit powinno być samowyjaśniające. Na uwagę zasługuje zwłaszcza wywołanie metody redirect, która przekierowuje żądanie do akcji list na zadanym kontrolerze (w tym przypadku będzie to ten sam kontroler). Jako zagadkę pozostawiam wyjaśnienie umieszczenia return po redirect.

Obiekt params jest zmienną mapą parametrów żądania.

I dopiero teraz pojawia się wyjaśnienie budowy adresu URL w Grails
 http://<serwer>:<port>/<kontekst>/<kontroler>/<akcja>/<parametr-klucz>/<parametr-wartosc>/...
Zabezpiczenie samej strony jest zdecydowanie niewystarczające, gdyż włamywacz mógłby spreparować pożądany adres URL samodzielnie i go wywołać bez wywołania strony GSP. Stąd koniecznym jest umieszczenie właściwej kontroli w kontrolerze. Dotyczy to dowolnej technologii webowej.

Wyrażenie return w Grails może zwrócić model, który będzie stosowany do wyświetlenia widoku - strony GSP, w której będą dynamicznie pobierane wartości z modelu, np.
 return [user: mojObiektUser]
Zatem wszystkie odwołania do obiektu user będą odwoływały się do modelu z user opartym na mojObiektUser. Można również zapisać to w równoważnej postaci
 [user: mojObiektUser]
gdyż w Groovy, a więc i w Grails, wynik ostatniego wyrażenia jest wynikiem wywoływanej metody.

Za pomocą konstrukcji
 <klasa-dziedzinowa>.properties = params
w kontrolerze przypisujemy pasujące parametry żądania do odpowiadających im atrybutom klasy dziedzinowej, co znacząco skraca ilość potrzebnego kodu do przypisania danych wprowadzonych przez użytkownika w formularzu do klasy dziedzinowej. Ten mechanizm nazywamy przypisywaniem danych (ang. data binding) i będzie wyjaśnione szerzej w kolejnym rozdziale. Korzystając z niego możemy przypisać pola klasy dziedzinowej korzystając z dynamicznie tworzonego konstruktora, np.
 def user = new User(params)
Grails rozpozna przekazane w mapie params (która jest mapą parametrów żądania, np. z formularza) dostępne dane i przypisze je do odpowiednich pól.

Jako, żę kontrola poprawności to sztandarowy przykład na użycie mechanizmów programowania aspektowego (ang. AOP - Aspect-Oriented Programming), więc pojawia się temat interceptorów (które znane są również z EJB3). Autorzy porównują je również z filtrami w Java Servlets oraz (ponownie) Ruby on Rails. W Grails dostępne są interceptory before oraz after, czyli, odpowiednio, zaraz przed wykonaniem i zaraz przed zakończeniem wykonania metody. Wystarczy zdefiniować domknięcie beforeInterceptor lub afterInterceptor w klasie kontrolera, aby uzyskać pożądaną funkcjonalność. Oba interceptory (metody przechwytujące) to domknięcia, więc mają pełny dostęp do wszystkich widocznych zmiennych kontrolera. Dzięki operatorowi bezpiecznego odczytu ?. mamy możliwość zapobieżenia NPE (= NullPointerException). Operator ?. sprawdza, czy aktualna część wyrażenia nie jest null, zanim rozpocznie wykonanie kolejnej części. Oba interceptory wywoływane są domyślnie dla każdej metody kontrolera, ale za pomocą warunków (wykonania) interceptora (ang. interceptor conditions) - atrybut except w definicji interceptora - ich wykonanie może być zawężone, np.
 def beforeInterceptor = [action:this.&beforeAudit, except:['list']]
gdzie action to domknięcie do wykonania przed wykonaniem wszystkich akcji kontrolera, poza akcją list.

Na zakończenie omawiany jest temat filtrów, które zbliżone są w działaniu do interceptorów, ale są definiowane w dedykowanej dla siebie klasie, której nazwa kończy się na Filters i mogą być związane z wieloma kontrolerami. Ich zastosowanie to umieszczenie danej funkcjonalności kontrolnej w jednym miejscu - filtrze i (warunkowe) związywanie z kontrolerami. Filtry aplikacyjne znajdują się w katalogu grails-app/conf, np.
 class UserFilters {
def filters = {
userModificationCheck(controller: 'user', action: '*') {
// tutaj treść filtru
}
someOtherFilter(uri: '/user/*') {
// tutaj treść filtru
}
}
}
W powyższym przykładzie (zapożyczonym z książki - Listing 5-41) związujemy filtr userModificationCheck ze wszystkimi akcjami (parametr action: '*') kontrolera UserController (parametr controller: 'user'), a someOtherFilter z adresem pasującym do wzorca /user/* (parametr uri: '/user/*'). Określenie wykonania filtra przez, po czy w obu przypadkach definiujemy przez wyrażenie before w ramach definicji filtra, np.
 userModificationCheck(controller: 'user', action: '*') {
before = {
// tutaj treść filtra
}
}
Wykonanie różnych czynności dla różnych akcji (kiedy zdefiniowano, że interesują nas wszystkie akcje - parametr action: '*') jest możliwe z
 if (actionName == 'edit') {
...
}
w ramach treści filtra.

W ramach komentarza autorzy opisują mechanizm odnotowywania komunikatów (ang. logging). Wszystkie kontrolery w Grails mają dostęp do atrybutu log typu org.apache.commons.logging.log. Domyślnie Apache Commons Logging pracuje na bazie Apache log4j. Konfiguracja odnotowywania komunikatów znajduje się w grails-app/conf/Config.groovy i domyślnie jest INFO.

Kolejny rozdział 6 "Building Domains and Services" jest równie bogaty, ale po nim i dzisiejszym pójdzie już z górki. Można będzie w końcu przymierzyć się do pierwszej grailsowej aplikacji.

p.s.I Przeczytałem na blogu Tomka Nurkiewicza - Elegancki CRUD w jednej akcji Struts2 część 1/2 o rozwiązaniu dla przekierowania żądania i utrzymywania komunikatów między żądaniami, których byt powinien być większy od żądania (request), a krótszy od sesji (session). Struts2 rozwiązuje to przez interceptor, a Grailsy przez zakres flash. Mimo wszystko zdumiało mnie przypadkowe(?) przedstawienie problemu, o którego rozwiązaniu właśnie przeczytałem w Grails. Jestem gotów nazwać styczeń 2009 miesiącem zbiegów okoliczności.

p.s.II W międzyczasie trafiłem na ciekawy projekt ze stajni Mozilla Labs - Mozilla Ubiquity. Warto zapoznać się z prezentacją na głównej stronie projektu (bodajże 5 minut), aby poznać ułatwienia, jakie oferuje nam projekt (daje również pojęcie, czego moglibyśmy oczekiwać w naszych aplikacjach webowych, aby były jeszcze bardziej powalające na kolana). Wystarczy zainstalować wtyczkę Ubiquity 0.1.5 i Ctrl+Spacja, aby w praktyce przekonać się o jego sile. Ja już mam. Na razie FF wstał bez problemów i działa z kilkoma poleceniami, więc całkiem nieźle ;-) Już skorzystałem chociażby z Ctrl+Spacja i fullscreen oraz Ctrl+Spacja i deli

p.s.III Pojawił się nowy egzamin na javaBLACKbelt - Groovy - Basic. Można, więc przejrzeć już dostępne pytania i poznać Groovy oczyma innych (w końcu pytania tworzone przez społeczność są właśnie tymi najbardziej wymagającymi, w których cechy produktu są naciągane do granic ich możliwości).

p.s.IV Jeden konkurs minął, a zaczął się kolejny - Bloger Roku 2008. Napiszę o nim szerzej w kolejnym wpisie - teraz tylko taka zajawka.

Brak komentarzy:

Prześlij komentarz