07 lutego 2008

Wicket i jego podejście zorientowane na widok

Wielu z Was pracujący z różnymi szkieletami programistycznymi do tworzenia aplikacji webowych zauważyła już zapewne wyłaniający się ich podział względem ich głównego zainteresowania - strona vs akcja. Dla mnie najbardziej interesującymi szkieletami wydają się być te, dla których w centrum zainteresowania znajduje się strona, dla której budowany jest model obiektowy (ang. page-centric approach), np. JSF czy GWT. Istnieją również takie, gdzie główny nacisk kładzie się na wykonywane akcje (ang. action-centric approach) jak np. Struts, gdzie programista tworzy zazwyczaj akcje reagujące na wybrane zdarzenia bez ich szczególnego związku ze stroną, z której są wywoływane. Nie wdając się w szczegóły napiszę, że Wicket jest zorientowany na stronę/widok, co jest zauważalne już przy pierwszym zetknięciu z nim. Istnieje bezpośrednia zależność między klasą w Javie reprezentującą stronę (dziedziczy po org.apache.wicket.markup.html.WebPage), a stroną HTML, która jej odpowiada. Po pierwsze ich nazwy muszą być identyczne z dokładnością do rozszerzenia (java dla klasy, a html dla strony). Druga niezwykle istotna cecha Wicketa to utworzenie modelu w klasie dziedziczącej po wspomnianym WebPage, który musi odpowiadać stronie HTML. Odstępstwo od tej zasady kończy się przykładowo następującym błędem:
ERROR - RequestCycle               - The component(s) below failed to render. 
A common problem is that you have added a component in code but forgot to reference it in the markup
(thus the component will never be rendered).

1. [MarkupContainer [Component id = anotherUserId, page = pl.jaceklaskowski.wicket.HomePage,
path = 0:loginForm:anotherUserId.TextField, isVisible = true, isVersioned = false]]

org.apache.wicket.WicketRuntimeException: The component(s) below failed to render.
A common problem is that you have added a component in code but forgot to reference it in the markup
(thus the component will never be rendered).

1. [MarkupContainer [Component id = anotherUserId, page = pl.jaceklaskowski.wicket.HomePage,
path = 0:loginForm:anotherUserId.TextField, isVisible = true, isVersioned = false]]

at org.apache.wicket.Page.checkRendering(Page.java:1102)
at org.apache.wicket.Page.renderPage(Page.java:899)
at org.apache.wicket.request.target.component.BookmarkablePageRequestTarget.respond(BookmarkablePageRequestTarget.java:231)
at org.apache.wicket.request.AbstractRequestCycleProcessor.respond(AbstractRequestCycleProcessor.java:103)
at org.apache.wicket.RequestCycle.processEventsAndRespond(RequestCycle.java:1100)
at org.apache.wicket.RequestCycle.step(RequestCycle.java:1169)
at org.apache.wicket.RequestCycle.steps(RequestCycle.java:1245)
at org.apache.wicket.RequestCycle.request(RequestCycle.java:535)
at org.apache.wicket.protocol.http.MockWebApplication.processRequestCycle(MockWebApplication.java:356)
at org.apache.wicket.protocol.http.MockWebApplication.processRequestCycle(MockWebApplication.java:341)
at org.apache.wicket.util.tester.BaseWicketTester.startPage(BaseWicketTester.java:289)
at pl.jaceklaskowski.wicket.TestHomePage.testRenderMyPage(TestHomePage.java:21)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:585)
at junit.framework.TestCase.runTest(TestCase.java:164)
at junit.framework.TestCase.runBare(TestCase.java:130)
at junit.framework.TestResult$1.protect(TestResult.java:106)
at junit.framework.TestResult.runProtected(TestResult.java:124)
at junit.framework.TestResult.run(TestResult.java:109)
at junit.framework.TestCase.run(TestCase.java:120)
at junit.framework.TestSuite.runTest(TestSuite.java:230)
at junit.framework.TestSuite.run(TestSuite.java:225)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:585)
at org.apache.maven.surefire.junit.JUnitTestSet.execute(JUnitTestSet.java:213)
at org.apache.maven.surefire.suite.AbstractDirectoryTestSuite.executeTestSet(AbstractDirectoryTestSuite.java:138)
at org.apache.maven.surefire.suite.AbstractDirectoryTestSuite.execute(AbstractDirectoryTestSuite.java:125)
at org.apache.maven.surefire.Surefire.run(Surefire.java:132)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:585)
at org.apache.maven.surefire.booter.SurefireBooter.runSuitesInProcess(SurefireBooter.java:290)
at org.apache.maven.surefire.booter.SurefireBooter.main(SurefireBooter.java:818)
Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 0.593 sec <<< FAILURE!

Results :

Tests in error:
testRenderMyPage(pl.jaceklaskowski.wicket.TestHomePage)

Tests run: 1, Failures: 0, Errors: 1, Skipped: 0

[INFO] ------------------------------------------------------------------------
[ERROR] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
Błąd wystąpił z powodu zadeklarowania
 new TextField("anotherUserId", new Model(""))
, podczas gdy odpowiadająca strona HTML nie zawierała elementu oznaczonego przez wicket:id="anotherUserId".

W stronie HTML, dla każdego elementu zawierającego atrybut wicket:id musi istnieć jego odpowiednik w reprezentacji obiektowej w Javie (z pewnymi wyjątkami).

Model, który jest jednym z najważniejszych pojęć Wicketa, jest źródłem danych dla komponentu. Komponentem nazywa się obiektowy odpowiednik elementu na stronie HTML z atrybutem wicket:id. W naszym przypadku następująca deklaracja odpowiada zasileniu pola tekstowego przez ciąg pusty.
 new TextField("userId", new Model(""))
Niektóre z komponentów Wicketa (uwaga na duże podobieństwo do JSF!) potrafią reagować na zdarzenia po stronie klienta (strona HTML). W naszym przypadku formularz na stronie (element form) ma odpowiednik w postaci klasy org.apache.wicket.markup.html.form.Form w modelu obiektowym (w Javie) i zatwierdzenie formularza skutkuje wywołaniem metody Form.onSubmit(). Istotna uwaga w JavaDoc dla klasy Form:

However, it is not necessary to use Wicket's button class, just putting e.g. <input type="submit" value="go"> suffices.

co powoduje, że jest to ten właśnie wspomniany wyjątek, gdzie element na stronie HTML nie musi mieć reprezentacji obiektowej w klasie dziedziczącej po WebPage.

Ciekawe jest pobieranie wartości z widoku (strona HTML) poprzez org.apache.wicket.model.PropertyModel. Wystarczy zarejestrować obiekt, którego wartość chcemy pobierać/ustawiać w danym elemencie na stronie i umieszczamy go w WebPage.
 public class HomePage extends WebPage {

private String login;

public String getLogin() {
return login;
}

public void setLogin(String login) {
this.login = login;
}

public HomePage(final PageParameters parameters) {

Form loginForm = new Form("loginForm") {
public void onSubmit() {
System.out.println("onSubmit wywolany z login: " + getLogin());
}
};
loginForm.add(new TextField("login", new PropertyModel(this, "login")));
add(loginForm);
}
}
Ostatecznie moja pierwsza aplikacja prezentuje się następująco.
package pl.jaceklaskowski.wicket;

import org.apache.wicket.PageParameters;
import org.apache.wicket.markup.html.WebPage;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.form.TextField;
import org.apache.wicket.model.PropertyModel;

public class HomePage extends WebPage {

private static final long serialVersionUID = 1L;

private String login;
private String password;

public HomePage(final PageParameters parameters) {

add(new Label("message", "If you see this message wicket is properly configured and running"));
Form loginForm = new Form("loginForm") {
public void onSubmit() {
System.out.println("onSubmit wywolany z login: " + getLogin());
}
};
loginForm.add(new TextField("login", new PropertyModel(this, "login")));
add(loginForm);
}

public String getLogin() {
return login;
}

public void setLogin(String login) {
this.login = login;
}
}
, gdzie odpowiadająca strona HTML jest jak następuje.
 <html>
<head>
<title>Wicket Quickstart Archetype Homepage</title>
</head>
<body>
<strong>Wicket Quickstart Archetype Homepage</strong>
<br/><br/>
<span wicket:id="message">message will be here</span>
<form wicket:id="loginForm" action="something">
Login: <input type="text" wicket:id="login"/>
<br>
<input type="submit" value="Wcisnij mnie"/>
</form>
</body>
</html>
Nad widokiem możnaby popracować, ale jak wspominałem daleko mi do HTMLowego mistrza sztuk walki, więc zostawiam to innym. Jak widać jest to o wiele bardziej możliwe niż w przypadku JSF, gdzie liczba nieznajomych znaczników może przerazić nawet najbardziej gorliwych praktyków technologii klienckich jak HTML, CSS, JavaScript i DHTML. Zauważam wiele podobieństw JSF do Wicketa i coraz bardziej zastanawiam się, który był inspiracją dla drugiego. Podobno rozpoczęły się prace nad wprowadzeniem Wicketa jako technologii "wizualizacji" alternatywnej do JSF w JBoss Seam. Zobaczymy, co z tego wyniknie. Jak na razie jestem pod dużym wrażeniem łatwości programowania z Wicket, chociaż nie ukrywam, że łączenie strony HTML z klasą WebPage za pomocą identyfikatorów wicket:id jest irytujące. Chciałoby się coś mniej "błędotwórczego" (chociaż na pytanie Co miałoby to być? nie znam odpowiedzi).

p.s. Kontynuując mój zachwyt dla Waszego zawzięcia z wysyłaniem SMSów w konkursie Blog Roku 2007 spieszę donieść, że Notatnik przesunął się na pozycję 9 z 49 głosami. Udało się więc przekonać kolejnych 6 czytelników Notatnika do zagłosowania wysyłając SMSa o treści B00248 pod numer 71222. Jeszcze brakuje do pierwszej trójki, ale idzie ku dobremu. Wierzę, że nie opuści Was nadzieja na 1. miejsce.

17 komentarzy:

  1. Jakiś czas temu też zainteresowałem się Wicketem, skoro wszyscy o nim mówią to musi być fajny.
    Jednak po przeczytaniu dokumentacji, próbie utworzenia kilku przykładów trochę się zniechęciłem.
    po pierwsze - podwójne tworzenie struktury strony - raz muszę utworzyć ją w HTMLu a drugi raz prawie to samo w Javie (tworzymy WebPage, do niej dodajemy Form, a do Form TextField)
    druga sprawa - to łączenie modelu ze stroną, jak tu z palca i sztywno przypisujemy dane do strony to jest ok, ale jak tak samo robi się np. w Struts to jest be!
    Wiem, że nie poznałem Wicketa dogłębnie, ale tak to jest pokazane we wstępnych przykładach, a nie coś bardziej zaawansowanego.
    Jednak dalej będę śledził twoje poczynania z Wicketem, może dowiem się czegoś nowego.

    OdpowiedzUsuń
  2. Ze Strutsem bawiłem się jeszcze przed wersją 1.0, kiedy wszyscy tworzyli servlety z mapowaniem *.do albo /action/* za zaleceniem Craiga (autora JServ i Struts i kilku innych rozwiązań). Było fajnie, ale mnie znudziło. Na pewno potrzebuję takiego rozwiązania jak JSF tylko bez tych znaczników na stronie JSP. Wicket podoba mi się właśnie z tej strony, ale podobnie jak Tobie nie podoba mi się ten związek strony HTML z WebPage. Coś mi tu nie leży.

    Napisz, co interesowałoby Cię najbardziej w przykładzie prezentującym Wicket, czego brakuje w tych dostępnych na jego stronach. Jaki przykład chciałbyś zobaczyć, aby go sprawdzić w działaniu? Siedzę i poznaję go, ale jak zawsze brakuje przykładów z prawdziwego zdarzenia. Co mogłoby tym być? Wprowadzenie JPA z pewnością uatrakcyjni prezentację Wicketa, ale co jeszcze?

    p.s. Nawet nie wyobrażasz sobie jak się ucieszyłem, kiedy po przeczytaniu Twojego komentarza wiedziałem o czym piszesz. Te wszystkie wicketowe rozwiązania, które wspomniałeś - w końcu je poznałem! ;-)

    OdpowiedzUsuń
  3. Hej - też pozwoliłem sobie na mały komentarz o Wicket na jdn.pl (http://jdn.pl/node/1380).

    Pozdrawiam,
    Waldek Kot

    OdpowiedzUsuń
  4. Mam nadzieję, że jak dojdziesz to JPA to zrobi się ciekawie, a na razie to ja nic nie zrozumiałem z twojego wpisu o Wickecie ;-( ale o tym w innym miejscu.

    OdpowiedzUsuń
  5. a.... i miałem jeszcze dopisać, że chyba w końcu napiszę coś o Struts 2 (nie formalnie WebWork 3 ;-)

    OdpowiedzUsuń
  6. Cześć Łukasz,

    Zaniepokoił mnie Twój komentarz odnośnie zrozumienia przedstawianego materiału. Czy to wynika z moich niezrozumiałych konstrukcji językowych, za mało przykładów, a za dużo gadania czy jeszcze coś innego? Napisz, bo pomoże mi to na skupienie się na ważnych rzeczach, a nie jakiś tam dyrdymałach. I gdybym mógł stać się bardziej zrozumiały to bardzo pomogłoby mi również na kolejnych konferencjach. Chciałbym również napisać artykuł o Wicket i Twoje spostrzeżenia byłyby bardzo cenne.

    OdpowiedzUsuń
  7. Ten komentarz odnosił się po części do twojego następnego wpisu nt. Wicketa ;-), ale najwyraźniej coś "zjadłem" i wyszła kicha!
    Jedyne co bym zmienił, to obcinał zrzuty listingów od wyjątków, trzy - cztery linijki będą wystarczające.

    OdpowiedzUsuń
  8. Mam pewien pomysł na lepsze związanie widoku (.html) z odpowiadającą klasą Java dziedziczącą po WebPage.

    Po pierwsze czemu nie mielibysmy sobie zrobić np. szablonu XSLT, który generuje nam szkielet "(My)WebPage.java" na podstawie HTML. HTML to ważny XML, a już reguła generowania na podstawie obecności widget:id=... jest zupełnie trywialna. Chyab zaraz nad tym siądę.

    Alternatywnie można pożyczyć ideę ze Struts'a, z klasy DynamicActionForm ... Możemy zrobić sobie coś w rodzaju DynamiCWebPage, która w kontruktorze, np. za pomocą refleksji dowiaduje się jak się nazywa związana strona HTML, wczytuje ją, a potem parsuje i dla każdego wicket:id dodaje sobie co trzeba. Jak jeszcze dorobimy sobie np. dodatkowy namespace (powiedzmy trojanwicket), a w nim np. atrybut type, to można dodawac komponenty właściwego typu.

    Muszę się jeszcze zastanowić nad kwestią co trzeba zrobić przed wywołaniem add(...), ale wydaje się, że sporo można zrobić. Ewentualnie zawsze można zrobić sobie dodatkowy "interceptor" w postaci metody któą możemy przesłonić, wykoniwanej przed automatycznie wygenerowanym kawałkiem konstruktora.

    OdpowiedzUsuń
  9. I tym to sposobem zrobiłeś założenia pod kolejny WebFramework ;-)

    OdpowiedzUsuń
  10. Krzysiek,

    Czy mógłbyś kontynuować temat, tzn. ja będę dalej zgłębiał Wicketa (dostarczał Ci dalszych informacji o nim), a Ty mógłbyś je udoskonalić. Bardzo podoba mi się Twój pomysł, ale nie będę ukrywał, że XSLT to była technologia, którą dotykałem wieki temu i wolałbym teraz w dobie adnotacji nie dotykać jej jeszcze. Na razie jestem pod wpływem adnotacji i niewielkiej ilości XMLi, więc XSLT sobie tymczasowo odpuszczam. Na bazie Twojego pomysły możnaby całkiem ciekawe rozszerzenie do Wicketa dorobić i cieszyłbym się, gdyby się Tobie udało, a ja mógłbym napisać, że byłem dla Ciebie natchnieniem, weną ;-)

    Jacek

    OdpowiedzUsuń
  11. Dobra, umówmy się tak - nie wtym tygodniu, bo mam w tej chwili sporo do zrobienia w weekend - ale zajmę się tym. Muszę co prawda jeszcze doczytać o Wicket'ach - bo tak naprawdę Jacku, to wiem o nich tyle co przeczytałem w Twoim blogu, Jacku.

    Co do samego XSLT to był przykład - wydaje się że najlogiczniejszy, skoro działamy na stronie (X)HTML. Ale można sobie wyobrazić też np. użycie Velocity - choć tu już trzeba by dodac jakiś elemencik do samego HTML - a to nie jest korzystne.

    I jeszcze ostatnie rzecz - napisać takiego XSLT to napiszę - ale to nie implikuje, że jestem ekspertem od XSL - jak ktoś się zna niech zrobi - będzie lepszy efekt - w kopyrajcie niech dorzucie "idea by Krzysztof Trojan" ( żart ;)

    OdpowiedzUsuń
  12. Mam pierwszą wersję, na pewno nie działa, choćby ze względu na zagnieżdzone cudzysłowy, ale na razie nie pamiętam jak to dokładnie się eskapuje, ale to szczegół). Nie pamiętam też czy wolno zagnieżdzać template ;) No i trzeba dodac trochę wersji dla róznych znaczników (input, submit, table, cholera wie co tam jeszcze). Ale idea na pewno może działać po poprawkach.

    tylko jak to wrzucić w ten blog ?

    OdpowiedzUsuń
  13. Pora na własny blog? Wydaje mi się, że dopiero się rozgrzewasz i wszystko co ciekawe przed nami. Możesz również opublikować swoje prace na pl.comp.lang.java. Ostatecznie możesz również przesłać do mnie, ale biorąc pod uwagę moją niewielką znajomość Wicketa oraz brak zainteresowania XSLT mogę nie dostrzeć wielkości dzieła ;-)

    Jacek

    OdpowiedzUsuń
  14. całości dzieła jeszcze nie ma. Co do miejsca do opublikowania, to by się znalazło, ale chciałem u Ciebie, żeby było w jednym wątku.

    Jak skończę, to wrzucę na swoją stronkę. Ale to jak mówiłem dopiero za jakiś tydziń jak znajdę chwilę.

    OdpowiedzUsuń
  15. No dobra, idea z grubsza jest jak poniżej:

    <?xml version="1.0" encoding="UTF-8"?>
    <xsl:stylesheet version="1.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xhtml="http://www.w3.org/1999/xhtml"
    xmlns:wicket="http://wicket.sourceforge.net">

    <xsl:output
    method="text"
    omit-xml-declaration="yes"
    />

    <xsl:template match="/">
    Opakowanie z przodu (czyli deklaracja pakietu, importy, public class XXX)

    <!-- Tu sobie wywolujemy transformacje tego, co jest zwiazane z wicket -->
    <xsl:apply-templates/>

    Opakowanie z tylu (koniec klasy i inne smieci
    </xsl:template>

    <xsl:template match="*[@wicket:id]">
    Tu sobie tworzymy co trzeba zwiazanego z danym elementem Wicket'a
    </xsl:template>


    </xsl:stylesheet>


    Ale nie jest tak różowo, żeby takie coś z biegu użyć. Po pierwsze, trzeba zadbać o to, aby dokument HTML był ważnym XML'em. Niby nic, bo przecież XHTML to XML prawda? No, prawda, tyle że dodajemy sobie do tego ważnego XHTML element wicket:id - i nie mówimy co to za bydle.

    Trzeba więc jeszcze w HTML dołożyć namespace dla wicket. A na dodatek nieco się nam komplikuje XSLT, bo od tej pory nie ma czegoś takiego jak "default" namespace. Trzeba sobie dodać explicite namespace dla HTML, i to tak aby identyfikator (URI) się zgadzał z dokuemntem przetwarzanym (co akurat łatwo zapewnić ;) - ja dodadałem powyżej xmlns:xhtml...

    OdpowiedzUsuń
  16. Więcej w następnym odcinku ;) - jest juz trochę, ale chcę to domknąć przed publikacją, a przy okazji nieco musze się douczyć ;) - i tyle z tego będzie pożytku przynajmniej.

    OdpowiedzUsuń
  17. Zgodnie z obietnicą, choć z wielkim opóźnieniem, przygotowałem szablon XSLT generujący klasy Java dla Wicket z HTML'a. Niestety, wczęsniej jakoś nie wyszło!

    Zainteresowanych zapraszam pod http://trojanbug.eu/index.php?navid=2&aid=0

    Pod tym adresem opisałem jak stworzyłem szablon (taki mini tutorial XSLT), oraz umieściłem kod (w linku na koncu artykułu).

    OdpowiedzUsuń