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.