Apache Wicket – trochę inny framework

Apache Wicket to komponentowy framework (szkielet) dla aplikacji WWW. Możecie sobie zadać pytanie po co jeszcze jedno narzędzie, skoro mamy Struts, JSF, Spring MVC itd…
Nudne wprowadzenie
Wicket jest alternatywą dla wyżej wymienionych, stara się zmniejszyć do minimum ilość metadanych zapisanych w dziesiątkach kilobajtów złożonych plików XML i przywrócić programiście możliwość pisania w Javie.
Wicket pozwala na oddzielenie kodu HTML od logiki aplikacji, nie dostarcza wielu gotowych komponentów, lecz umożliwia łatwe tworzenie własnych i rozszerzanie istniejących.
Framework bazuje bezpośrednio na specyfikacji serwletów, w oparciu o nią pozwala na budowę aplikacji z komponentową warstwą widoku. Inne rozwiązania tego typu, napisane w Javie, zazwyczaj wymuszają użycie stron JSP. Takie rozwiązanie, w wielu przypadkach wygodne, słabo nadaje się do realizowania podejścia obiektowego, czasami wymusza łamanie jego zasad. Dynamiczne generowanie HTML-a realizowane jest tam poprzez mieszanie tagów JSTL, wyrażeń EL, kodu HTML, fragmentów w Javie. W rezultacie powstaje trudny do odczytania i pielęgnacji kod podobno przypominający spaghetti :-). Moim skromnym zdaniem to porównanie jest obrazą dla tak zacnego posiłku.
W zależności od preferencji rezygnacja z metadanych zapisanych w plikach XML może być wadą lub zaletą. Mocnymi stronami takiego rozwiązania są lepsza podatność kodu na zmiany i możliwość korzystania ze standardowych narzędzi do refaktorowania kodu.
Pojedynczy komponent w Wicket składa się z dwóch części: kodu w Javie oraz pliku HTML. Komponenty mogą być dziedziczone. Dotyczy to zarówno części w HTML jak i w Javie.
Plik HTML korzysta ze standardowych znaczników, elementy obce są ograniczone do atrybutów wicket:id identyfikujących poszczególne komponenty oraz kilku znaczników specyficznych dla Wicket. Znaczniki te odpowiadają za wprowadzanie punktów w których kontrolę przejmuje kod w Javie.
Zachowania dynamiczne (pętle, bloki wyświetlane po spełnieniu pewnych warunków) są obsługiwane w pełni w części Javowej komponentów. Taka organizacja umożliwia podział odpowiedzialności pomiędzy programistę i projektanta WWW, pliki HTML komponentów można zmieniać za pomocą standardowych narzędzi do tego przeznaczonych.
Do roboty!
Kody źródłowe to tego przykładu są tu.
Autorzy Wicket rekomendują projekt Apache Maven 2 jako narzędzie do budowania projektów opartych o ten szkielet. Rzeczywiście stworzenie szkieletu aplikacji z wykorzystaniem tego narzędzia jest łatwe i przyjemne :). Opis instalacji Mavena można znaleźć na stronie projektu.
Po zainstalowaniu narzędzia wydajemy polecenie:
mvn archetype:create -DarchetypeGroupId=org.apache.wicket -DarchetypeArtifactId=wicket-archetype-quickstart -DarchetypeVersion=1.3.4 -DgroupId=pl.j2ee.wicket -DartifactId=j2ee_wicket
j2ee_wicket będzie nazwą naszego projektu.
pl.j2ee.wicket to nazwa pakietu, w którym znajdziemy wygenerowany kod.
Projekt można łatwo importować do ulubionego IDE. W przypadku eclipse warto użyć wtyczki m2eclipse, w nowszych wersjach dobrze integrującej się z WTP – zestawem wtyczek dla programistów aplikacji webowych.
Po wykonaniu tych kroków mamy gotowy do uruchomienia prosty projekt. Pozostaje nam tylko uruchomienie klasy j2ee.pl.Start jako zwykłej aplikacji Javowej i nakierowanie przeglądarki na http://localhost:8080/j2ee_wicket. Projekt można uruchomić również z poziomu mavena – z celem jetty:run:
mvn install mvn jetty:run
Jeżeli udało się wszystko dobrze skonfigurować, oczom naszym powinien ukazać się tryumfalny komunikat:
Wicket Quickstart Archetype Homepage
If you see this message wicket is properly configured and running
Jako że j2ee.pl.Start jest zwykłą klasą Javową debugowanie naszego projektu będzie równie łatwe jak to jest w przypadku zwykłej aplikacji dla platformy JSE.
Na początek możemy sprawić by aplikacja robiła coś więcej. Zacznijmy od dodania klasy która będzie implementować prostą logikę:
package pl.j2ee.wicket.model;
import java.io.Serializable;
public class Suma implements Serializable {
private static final long serialVersionUID = 6261687669280596713L;
private Integer skladnik1;
private Integer skladnik2;
public Integer getSkladnik1() {
return skladnik1;
}
public void setSkladnik1(Integer skladnik1) {
this.skladnik1 = skladnik1;
}
public Integer getSkladnik2() {
return skladnik2;
}
public void setSkladnik2(Integer skladnik2) {
this.skladnik2 = skladnik2;
}
public Integer getSuma() {
if (skladnik1 != null && skladnik2 != null) {
return new Integer(skladnik1.intValue() + skladnik2.intValue());
}
return null;
}
}
Dodajemy szablon SumaPage.html w pakiecie pl.j2ee.wicket
<html>
<head>
<title>J2ee wicket</title>
</head>
<body>
<span wicket:id="komunikat">miejsce na statyczny komunikat</span> <form wicket:id="dodaj">
<input type="text" wicket:id="skladnik1" />
<input type="text" wicket:id="skladnik2" />
<input type="submit" wicket:id="sumuj" />
</form>
<span wicket:id="suma"> wynik obliczen </span>
</body>
</html>
Wartości w atrybutach wicket:id będą identyfikatorami komponentów zdefiniowanych w SumaPage.java
package pl.j2ee.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.Button;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.form.TextField;
import org.apache.wicket.model.BoundCompoundPropertyModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.model.PropertyModel;
import pl.j2ee.wicket.model.Suma;
/**
* Suma
*/
public class SumaPage extends WebPage {
private static final long serialVersionUID = 1L;
protected Label resultLabel;
protected Suma suma;
public SumaPage(final PageParameters parameters) {
add(new Label("komunikat", "Podaj dwie liczby całkowite"));
suma = new Suma();
Form dodajForm = new Form("dodaj", new BoundCompoundPropertyModel(suma));
TextField skladnik1 = new TextField("skladnik1");
dodajForm.add(skladnik1);
TextField skladnik2 = new TextField("skladnik2");
dodajForm.add(skladnik2);
resultLabel = getResultLabel();
add(resultLabel);
dodajForm.add(getSubmitButton(dodajForm));
add(dodajForm);
}
protected Label getResultLabel() {
return new Label("suma", new PropertyModel(suma, "suma"));
}
protected Button getSubmitButton(Form form) {
return new Button("sumuj", new Model("Sumuj"));
}
}
Klasa SumaPage definiuje główny komponent dla strony o tej samej nazwie. Strona jest budowana poprzez organizowanie komponentów w drzewo. Węzły w tym drzewie stanowią obiekty implementujące interfejs Component. Tylko obiekty klas implementujących interfejs MarkupContainer (kontenery) mogą zawierać inne komponenty.
Jednym z tego typu komponentów jest formularz:
Form dodajForm = new Form("dodaj",new BoundCompoundPropertyModel(suma));
W tej linii definiujemy formularz o identyfikatorze dodaj (taki sam jak w pliku HomePage.html), oraz model powiązany z tym komponentem (BoundControlPropertyModel). Wicket zawiera wiele typów modeli, ten ma największe możliwości, pozwala komponentom składowym na dostęp do pól składowych obiektu modelu kontenera.
Według przyjętej konwencji jeżeli dla komponentu składowego formularza nie podano modelu, wicket próbuje powiązać ten komponent z modelem formularza interpretując identyfikator komponentu jako nazwę pola w obiekcie modelu formularza.
Nazwy skladnik1 i skladnik2 muszą być takie same jak nazwy pól w obiekcie suma.
TextField skladnik1 = new TextField("skladnik1");
dodajForm.add(skladnik1);
TextField skladnik2 = new TextField("skladnik2");
dodajForm.add(skladnik2);
Musimy dodać przycisk wysyłający formularz i podpiąć formularz do strony. Klasa Model definiuje niemodyfikowalny model, w tym przypadku zawsze będzie to podany String.
dodajForm.add(getSubmitButton(dodajForm));
add(dodajForm);
.
.
.
protected Button getSubmitButton(Form form) {
return new Button("sumuj", new Model("Sumuj"));
}
Rezerwujemy miejsce w którym zobaczymy wynik obliczeń. PropertyModel działa podobnie do BoundControlPropertyModel lecz wiąże tylko jedno pole w beanie. Z modelu tej klasy możemy skorzystać też w elemencie formularza.
resultLabel = getResultLabel();
add(resultLabel);
.
.
.
protected Label getResultLabel() {
return new Label("suma", new PropertyModel(suma, "suma"));
}
By dostać się na nowo dodaną stronę, musimy dodać odnośnik na stronie głównej, odpowiednie zmiany w HomePage.java i HomePage.html:
add(new PageLink("sumaLink", SumaPage.class));
<br/><a wicket:id="sumaLink">Suma</a>
Pozostaje uruchomienie przykładu i radość z osiągniętych wyników :-).
Wicket i AJAX
Wicket zawiera ciekawą obsługę technologii AJAX. Aby ją wykorzystać w naszym przykładzie wystarczy mała modyfikacja klasy SumaPage. Ale możemy też wykorzystać mechanizm dziedziczenia:
Dodajemy klasę SumaAjaxPage:
package pl.j2ee.wicket;
import org.apache.wicket.PageParameters;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.markup.html.form.AjaxButton;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.Button;
import org.apache.wicket.markup.html.form.Form;
/**
* Suma i ajax
*/
public class SumaAjaxPage extends SumaPage {
protected Label getResultLabel() {
Label resultLabel2 = super.getResultLabel();
resultLabel2.setOutputMarkupId(true);
return resultLabel2;
}
protected Button getSubmitButton(Form form) {
return new AjaxButton("sumuj", form) {
private static final long serialVersionUID = -7912898126800181416L;
protected void onSubmit(AjaxRequestTarget target, Form dodajForm) {
target.addComponent(resultLabel);
}
};
}
public SumaAjaxPage(PageParameters parameters) {
super(parameters);
}
}
Metoda onSubmit w obiekcie klasy anonimowej dziedziczącej z AjaxButton jest wywoływana po naciśnięciu przycisku Sumuj w wyniku asynchronicznego zapytania z przeglądarki. W tej metodzie możemy zadeklarować które z komponentów powinny być odświeżone. Wicket wysyła w odpowiedzi fragmenty kodu HTML odpowiadające wybranym komponentom. Po stronie przeglądarki w odpowiedni sposób zmieniane jest drzewo DOM strony. Aby taka podmiana była możliwa, komponenty powinny posiadać atrybut id, który domyślnie nie jest obecny. Aby to zmienić należy wykonać na odświeżanych komponentach w momencie ich tworzenia metodę setOutputMarkupId(true). W naszym przykładzie oznacza to dodanie linii:
resultLabel2.setOutputMarkupId(true);
Na stronie głównej dodajemy link do SumaAjaxPage:
add(new PageLink("sumaAjaxLink", SumaAjaxPage.class));
<br/><a wicket:id="sumaAjaxLink">Suma Ajax</a>
Po wejściu przez odnośnik Suma Ajax przyciśnięcie sumuj nie powoduje przeładowania strony, lecz tylko odświeżenie fragmentu z rezultatem. Dlaczego ten przykład działa skoro nie dodaliśmy pliku SumaAjaxPage.html? To kolejna przyjemna cecha Wicketa – dziedziczona może być nie tylko implementacja w Javie, ale też szablony HTML, po prostu strona SumaAjaxPage używa szablonu SumaPage.html !
Testowanie
Na stronie pojawił się element którego nie dodawaliśmy (odnośnik opisany WICKET AJAX DEBUG). Wynika to z tego że aplikacja jest uruchomiona jest w trybie debug. Po kliknięciu w ten odnośnik wyświetla się okno, w którym możemy zobaczyć wywołania AJAX z przeglądarki i odpowiedzi na te zapytania, coś co bardzo przydaje się przy debugowaniu bardziej złożonych aplikacji.
Wicket zawiera klasę WicketTester służącą do testów jednostkowych komponentów. Została napisana z użyciem asercji JUnit, ale działa również z TestNG. Za pomocą tego narzędzia w dużym stopniu można zamodelować sposób używania aplikacji przez użytkownika, można sprawdzić nie tylko pojedyncze komponenty ale też scenariusze obejmujące wiele stron WWW. Testowalne są również akcje AJAX!
Testy dla opisanych tu komponentów znajdują się w załączonym kodzie źródłowym.
Co dalej?
Wicket stosunkowo łatwo rozszerzać. Dowodem na to może być choćby zestaw zewnętrznych modułów zgrupowanych w projekcie Wicket stuff .
Dokumentacja do Wicket nie jest rewelacyjna i daleko jej do kompletności, ale otwarte kody źródłowe są na tyle dobrze napisane, że braki łatwo uzupełnić po prostu je przeglądając. W trudnych przypadkach warto zadać pytanie na liście dyskusyjnej wicket-user, odpowiedzi na dobrze zadane pytania przychodzą szybko :-)








Recent Comments