15 lutego 2008

Ciekawostek Wicketa ciąg dalszy - wtyczka Wicket Bench, DropDownChoice, FeedbackPanel oraz UTF-8

Wicket coraz bardziej podobny do JSF. To co znajduję w JSF mam w Wicket, ale nie koniecznie na odwrót. Na pierwszy plan wysuwa się oddzielenie czegokolwiek, co związane z Wicket poza HTML (poza elementem wicket:id), co nie jest spotykane w przypadku innych szkieletów aplikacyjnych (za wyjątkiem bodajże Tapestry, którego znam bardzo pobieżnie). Mamy czyściutką stronę HTML bez dodatkowych udziwnień. Ma to swoje zalety, ale przyzwyczajony do JSP nie mogę pozbyć się nawyków włączenia czegoś w treść strony HTML z JSP, a mianowicie jego znaczników. Osobiście doświadczam ciekawej reakcji odwykowej polegającej na zmianie myślenia o tworzeniu aplikacji webowych bez wykorzystania JSP. Taką samą terapię przechodziłem ewaluując GWT i było to początkowo zabawne, później denerwujące, aż w końcu zobojętniałem i nawet znajdowałem w tym przyjemność. Teraz zdaje się, że będzie podobnie.

Zacząłem od aktualizacji Eclipse 3.4M5 o wtyczkę Wicket Bench 0.5.x, której i tak w zasadzie nie używam. Coś nie działa jak powinna podczas edycji stron HTML i kończy się z błędem NPE.


Miałem opisać jej instalację i możliwości, bo mamy nowy typ projektu, nowy edytor (który nie działa) i kilka innych udogodnień, ale co mi po wtyczce z bardzo podstawową funkcjonalnością "okraszoną" NPE. Wtyczka idzie do kosza.

Wracając do rzeczy, które działają i mają związek z Wicket 1.3.1. Do swojej demonstracyjnej aplikacji opartej o Wicket wdrożyłem org.apache.wicket.markup.html.form.DropDownChoice, który jest odpowiednikiem select w HTML, czyli rozwijalnej listy wyboru. Oto zmodyfikowana strona DaneOsobowe.html:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Wicket Demo App</title>
</head>
<body>
<strong>Wicket Demo App</strong>
<span wicket:id="komunikaty"></span>
<form wicket:id="daneOsobowe" action="">
Imię: <input type="text" wicket:id="imie" /> <br>
Nazwisko: <input type="text" wicket:id="nazwisko" /> <br>
Login: <input type="text" wicket:id="login" /> <br>
Miejscowość:
<select wicket:id="miejscowosc">
<option>Cokolwiek</option>
</select>
<input type="submit" value="Zatwierdź" />
</form>
</body>
</html>
z elementem <select wicket:id="miejscowosc">. Wartości w <option> są nieistotne i zostaną podmienione podczas działania aplikacji przez wartości z odpowiedniego modelu. Zresztą dużo by o tym pisać, a wystarczy zaprezentować odpowiadającą tej stronie klasę pl.jaceklaskowski.wicket.DaneOsobowe i wszystko stanie się jasne.
package pl.jaceklaskowski.wicket;

import java.util.Arrays;

import org.apache.wicket.PageParameters;
import org.apache.wicket.markup.html.WebPage;
import org.apache.wicket.markup.html.form.DropDownChoice;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.form.RequiredTextField;
import org.apache.wicket.markup.html.form.TextField;
import org.apache.wicket.markup.html.panel.FeedbackPanel;
import org.apache.wicket.model.CompoundPropertyModel;

import pl.jaceklaskowski.wicket.entities.Osoba;

public class DaneOsobowe extends WebPage {

private static final long serialVersionUID = 1L;

public DaneOsobowe(PageParameters params) {
Osoba osoba = new Osoba();
CompoundPropertyModel model = new CompoundPropertyModel(osoba);
Form loginForm = new Form("daneOsobowe", model) {
private static final long serialVersionUID = 1L;

protected void onSubmit()
{
// TODO: miejsce dla JPA
setResponsePage(new Powitanie(getModelObjectAsString()));
}
};
TextField imie = new TextField("imie");
// określenie pola obowiązkowego
imie.setRequired(true);
loginForm.add(imie);
// pomocnicza klasa wykonująca setRequired(true)
TextField nazwisko = new RequiredTextField("nazwisko");
loginForm.add(nazwisko);
TextField login = new RequiredTextField("login");
login.setRequired(true);
loginForm.add(login);
// lista rozwijalna z danymi z Osoba.getMiejscowosci (model formularza to Osoba)
loginForm.add(new DropDownChoice("miejscowosc",
Arrays.asList(new String[] { "Warszawa", "Krak\u00f3w",
"Wroc\u0142aw", "Pozna\u0144", "Szczecin", "Gda\u0144sk" })) {

private static final long serialVersionUID = 1L;

// wyślij wybór z listy po zmianie wyboru do serwera
@Override
protected boolean wantOnSelectionChangedNotifications() {
return true;
}

protected void onSelectionChanged(final Object newSelection)
{
System.out.println("Miejscowosc: " + newSelection);
}
});
add(loginForm);
// panel komunikatów
add(new FeedbackPanel("komunikaty"));
}
}
Dodałem do niej kilka komentarzy wyjaśniających dla poprawienia czytelności. Na chwilę obecną skoncentrujmy się na linii z new DropDownChoice("miejscowosc", Arrays.asList(new String[] { "Warszawa", "Kraków", "Wrocław", "Poznań", "Szczecin", "Gdańsk" })). Z nazwą miejscowosc związuję dane z listy. Wynik jaki będzie wiadomo - lista zostanie wypełniona danymi z listy. Jako, że lista rozwijalna związana jest z formularzem, który związany jest z modelem reprezentowanym przez klasę pl.jaceklaskowski.wicket.entities.Osoba konieczne było dopisanie metod zapisu i odczytu (mój nowy pomysł na tłumaczenie setters i getters ;-)) - public String getMiejscowosc() oraz public void setMiejscowosc(String miejscowosc):
package pl.jaceklaskowski.wicket.entities;

import java.io.Serializable;
import java.text.MessageFormat;

public class Osoba implements Serializable {

private static final long serialVersionUID = 1L;
private String imie;
private String nazwisko;
private String login;
private String miejscowosc;

public String getImie() {
return imie;
}

public void setImie(String imie) {
this.imie = imie;
}

public String getNazwisko() {
return nazwisko;
}

public void setNazwisko(String nazwisko) {
this.nazwisko = nazwisko;
}

public String getLogin() {
return login;
}

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

public String getMiejscowosc() {
return miejscowosc;
}

public void setMiejscowosc(String miejscowosc) {
this.miejscowosc = miejscowosc;
}

public String toString() {
return MessageFormat.format("{0} {1} - login: {2}", imie, nazwisko, login);
}
}
Powód? Wiadomy. Ustawienie wybranej przez użytkownika wartości z listy w modelu.

Dodatkowo należy omówić anonimową klasę wewnętrzną na bazie DropDownChoice w DaneOsobowe, gdzie nadpisałem metody protected boolean wantOnSelectionChangedNotifications() oraz protected void onSelectionChanged(final Object newSelection). Pierwsza to ustawienie aplikacji, aby każdorazową zmianę w liście rozwijalnej przesyłał na serwer, a drugą to wypisanie wybranej wartości. Proste? Pewnie!


Lista rozwijalna za nami. Pora zabrać się za kolejną zmianę - skorzystanie z klasy org.apache.wicket.markup.html.panel.FeedbackPanel, której celem jest wyświetlanie komunikatów związanych z walidacją (kontrolą poprawności?) oraz konwersją danych i aplikacyjnych w ogólności. Czy komuś przypomina to coś? Może coś pokrewnego z JSF? No jasne! Kontrolka h:message. Czyż Wicket i JSF to nie to samo? Pewnie, że nie - dużo podobieństwa, ale zaczynam być zdania, że wyłącznie na korzyść Wicketa. Dla zobrazowania działania FeedbackPanel dodałem wymagane pola w formularzu oparte o org.apache.wicket.markup.html.form.TextField wraz z jego metodą setRequired(boolean) oraz klasą pomocniczą będącą połączeniem TextField oraz setRequired(true) - org.apache.wicket.markup.html.form.RequiredTextField . Po umieszczeniu <span wicket:id="komunikaty"></span> na stronie HTML wystarczy spróbować zatwierdzić formularz z niepełnymi danymi i otrzymamy komunikaty błędów w panelu komunikatów.


Pozostaje na koniec wspomnieć o kodowaniu znaków na stronach HTML w Wicket. Spostrzegawcze oko zauważyło zapewne na stronie DaneOsobowe.html następującą deklarację <?xml version="1.0" encoding="UTF-8"?>. To jest sposób w Wicket na ustawienie kodowania stron HTML, więc nie pomagały inne deklaracje na stronie jak meta http-equiv="Content-Type" oraz odpowiednie kodowanie znaków w samym edytorze podczas edycji pliku. Konieczny jest nagłówek <?xml version="1.0" encoding="UTF-8"?>, który po wielu trudach pozwolił mi cieszyć się polskimi czcionkami w przeglądarce.

p.s. Czy ja się nie narzucam z tymi SMSami i ciągłym nagabywaniem na udział w konkursie Blog Roku 2007? Celem uszczęśliwienia Jacka proponuję wysłać SMSa o treści B00248 na numer 71222 (koszt 1,22 zł brutto), co ma niebagatelny wpływ nie tylko na jego samopoczucie, ale przede wszystkim na pozycję Notatnika w konkursie. Notatnik znajduje się na 7. miejscu z 83 głosami. Do setki zostało 17 głosów. Wszystkim, którzy już zagłosowali bardzo dziękuję, a niezdecydowanych zachęcam.

3 komentarze:

  1. Czy w Wicket jest jakiś mechanizm templetów? Powtarzanie za każdym razem całej strony HTML nie jest zbyt szczęśliwe ;-)
    Z tego co zrozumiałem to ze zmianą wartość w DropDown mamy związaną akcję Ajaxową, która przesyła wartość na serwer? A czy zwykłe pola tekstowe też mają taką właściwość, bo w przypadku submit na formularzu znów prześlemy tą samą wartość na serwer ;-)

    OdpowiedzUsuń
  2. @Łukasz: Co do szablonów stron to można skorzystać z tagów wicket:child/wicket:extend - na stronie bazowej definiuje się obszar, który będzie mógł być uzupełniony HTMLem strony dziedziczącej z bazowej. Minus tego rozwiązania jest taki, że w obecnej wersji można umieścić tylko jeden taki obszar na stronie (na razie nie udało się jeszcze przekonać twórców Wicketa by dodali moźliwość definiowania większej liczby takich obszarów).
    Lepszym rozwiązaniem jest tu użycie paneli, które można traktować jako bardzo dobre narzędzie do tworzenia stron z "klocków" - czyli swego rodzaju mechanizm szablonów ;).
    Jeżeli chciałbyś informować serwer o zmianie wartości w plu tekstowym, wystarczy do niego podpiąć odpowiedni ajax behavior.

    @Jacek: Kodowanie HTMLa można też ustawić w kodzie w metodzie init() aplikacji:
    getMarkupSettings().setDefaultMarkupEncoding("UTF-8");
    Ale nie twierdzę, że jest to lepsze rozwiązanie od podanego przez Ciebie.

    OdpowiedzUsuń
  3. Kurczę, dzięki z tym UTF-8!
    Walczyłem z tym ponad godzinę, dopóki nie wpadłem na ten blog.
    Dzięki!

    OdpowiedzUsuń