Comments
Description
Transcript
2013.10.17 第2回Wicket講習会資料完全版 - Wicket
Wicketによる モダンなWebアプリケーション Wicket-Sapporo 2013-10-17 Hiroto Yamakawa 自己紹介 • 山川広人(@gishi_yama) • [email protected] • 大学で教育支援システムの研究開発・運用に従事 • Apache Wicketを使い始めて6年ぐらい • Wicket大好き • Wicket-Sapporoコミュニティはじめました Wicketの概要と 動作の基本 (前回のふりかえり) • WicketはJavaプログラマがWebアプリケーシ ョンを快適に開発できる事を狙っている • HTMLをほぼそのまま使える(wicket:idをつ けるだけ) • 2種類のオブジェクト(コンポーネント、モデ ル)を使って、wicket:idをつけたタグを書き 換えることでWebページを構築する Wicketでは HTMLとJavaが一組 <body> <p wicket:id="label1">message1 is here.</p> </body> HTMLでは、 内容を書き換えたいタグに wicket:id をつける import org.apache.wicket.markup.html.WebPage; public class SimplePage extends WebPage { public SimplePage() { // コンストラクタ } } Javaでは、 ① WebPageのサブクラス を作成する // 1. 表示したいデータ(オブジェクト)を用意 String message1 = "Wicket-‐Sapporoにようこそ!"; // 2. 表示したいオブジェクトをModelオブジェクトでラップ IModel<String> model1 = new Model<>(message1); ② 表示したいデータを 取り扱うためのModelを 用意する // 3. Modelを、対応するHTMLのwicket:id用のコンポーネントにセット Label label1 = new Label("label1", model1); // 4. コンポーネントをページにadd add(label1); ③ 表示方法を決める コンポーネントにModelを セットし、ページに追加 (add)する ※コンポーネントの第1引数が wicket:id <body> <p wicket:id="label1">Wicket-‐Sapporoにようこそ!</p> </body> ∼実行結果∼ wicket-idがつけられたタグの 内容が書きかわる ※ wicket:idはdebugモードからdelploymentモードに切り替えると自動的に消える <body> <p wicket:id="label1">message1 is here.</p> </body> Label(“label1”) Wicket:idが一致するHTMLタグの値を上書き getObject 文章表示コンポーネント オブジェクト Model データ取扱オブジェクト String “WicketSapporoにようこそ!” return <body> <p wicket:id="label1">Wicket-‐Sapporoにようこそ!</p> </body> 入力フォームの コード例 送信された文字列をコンソールに表示する FormPage.html <form wicket:id="form"> <label>名前</label><br /> <input type="text" wicket:id="name"><br /> <label>年齢</label><br /> <input type="text" wicket:id="age"><br /> <input type="submit"> </form> FormPage.java public class FormPage extends WebPage { private IModel<String> nameModel; private IModel<Integer> ageModel; public FormPage() { nameModel = Model.of(""); ageModel = Model.of(0); Form<Void> form = new Form<Void>("form") { @Override protected void onSubmit() { super.onSubmit(); System.out.println(nameModel.getObject()); System.out.println(ageModel.getObject()); } }; add(form); form.add(new TextField<>("name", nameModel)); form.add(new TextField<>("age", ageModel)); } } public class Bean implements Serializable { private String name; private int age; Modelを変えると @Override データの取り扱い public String toString() { return "[name : " + name + "\nage : " + age + "]"; 方法を変えられる } //アクセッサは省略 public class FormPage extends WebPage { } public FormPage() { 前ページの Form.javaを Modelを変えて 書き換えたもの Form<Bean> form = new Form<Bean>( "form", new CompoundPropertyModel<>(new Bean())) { @Override protected void onSubmit() { super.onSubmit(); System.out.println(getModelObject().toString()); } }; add(form); form.add(new TextField<>("name")); form.add(new TextField<>("age")); } } 今日の内容 • Stateful • Clean URL • Ajax • Session StateFul Wicketでは、LinkやFormを利用した ページの状態を覚えている ステートフル! Page 1 Link Page 2 Link Page 3 History Back × 2 Link Page 4 例)前の加算結果に新しい値を足していく機能 885 1791 ブラウザの戻る2回 885 最初のページの状態 (計算結果)を 自動的に保持している Link new Link<T>("id", IModel<T>) { @Override public void onClick() { // リンクやボタンが押された時の処理 } }; リンクやボタン用のコンポーネント onClick() メソッドの中で setResponsePage(WebPageのサブクラスのインスタンス); の様に書くと、インスタンスを次ページとして遷移させる(遷移先もステートフル) Form new Form<T>("id", IModel<T>) { @Override public void onSubmit() { // Submitボタンがクリックされた時の処理。 } }; Formタグの大枠となるコンポーネント ページの遷移もさせたい場合はLinkと同様に setResponsePageメソッドを書く。 Submitボタンに特殊なものを使いたい時は、Buttonコンポーネントなどを利用できる。 LinkやFormが貼り付けられているページや遷移した先のペ ージのURLにつくPageIdごとに、ページの状態が自動的に 保存される。保存先はSessionとディスク。 PageIdを指定すると、そのときの状態のページが復元され る(バージョン管理的)。 PageIdは初期設定では連番だが、推測されたくない時は URLのcrypt暗号化やPageIdの乱数化が有効。 Clean URL Clean URL ?xxx=yyy... の様なURL(クエリ文字列)を /xxx/yyy/... の様なURL(クエリ文字列)に WicketでClean URLな クエリ文字列を使うには、 1.ブックマーク可能リンクを使う (BookmarkablePageLink) 2.URLとクエリ文字列の形式を登録 する(MountedMapper) BookmarkablePageLink new BookmarkablePageLink<T>("id", クラス); ブックマーク可能なURLでページを遷移する Linkコンポーネント PageParameters params = new PageParameters(); params.add("param1", "1000"); add(new BookmarkablePageLink<Void>("id", クラス, params)); クエリパラメータを渡す時は、PageParametersオブジェクトを用意し、 BookmarkablePageの第3引数に渡す //通常のリンク add(new Link<Void>("link") { @Override public void onClick() { setResponsePage(new IdReceiptPage()); } }); //ブックマーク可能リンク add(new BookmarkablePageLink<Void>("bpLink", IdReceiptPage.class)); 通常のリンクで遷移した IdReceiptPage ページのURL http://localhost:8080/wicket_workshop/ws01/wicket/page?1 ブックマーク可能リンクで遷移した IdReceiptPage のURL http://localhost:8080/wicket_workshop/ws01/wicket/bookmarkable/ org.wicket_sapporo.workshop01.page.bookmarkable.IdReceiptPage http://localhost:8080/wicket_workshop/ws01/wicket/bookmarkable/ org.wicket_sapporo.workshop01.page.bookmarkable.IdReceiptPage ブックマーク可能リンクでは、 クラスのパッケージ名+クラス名がURLとして表示されるが.... public class WS01Application extends WebApplication { @Override protected void init() { super.init(); // ...中略 mount(new MountedMapper("/id_receipt", IdReceiptPage.class)); } } WebApplication のサブクラスで MountedMapper オブジェクトを使えば... http://localhost:8080/wicket_workshop/ws01/id_receipt シンプルなURLになるよう設定できる MountedMapper mount(new MountedMapper("/PathToPage", クラス)); あるクラスにブックマーク可能なURLをマッピングする クエリ文字列の形式も設定できる mount(new MountedMapper("/Path", クラス, new UrlPathPageParametersEncoder())); 第3引数に UrlPathPageParametersEncoder を渡すと、 Clean URL の形式でクエリ文字列を設定できるようになる mount(new MountedMapper("/Path/${param1}", クラス); パスに ${パラメータ名} を加えると、その部分をクエリ文字列(の値)として扱う 利用例 WebApplicationのサブクラス#init() mount(new MountedMapper("/id_receipt", IdReceiptPage.class, new UrlPathPageParametersEncoder())); 遷移元ページクラスのコンストラクタ PageParameters params = new PageParameters(); params.add("param1", "1000"); add(new BookmarkablePageLink<Void>("id", IdReceiptPage.class, params)); ページ遷移時(IdReceiptPage.class)のURL http://localhost:8080/wicket_workshop/ws01/id_receipt/param1/1000 利用例 遷移先ページクラスのコンストラクタ public IdReceiptPage(PageParameters params) { if (params != null) { String param1 = params.get("param1").toString("パラメータなし"); String param2 = params.get("param2").toString("パラメータなし")); } // 以下略 クエリパラメータはコンストラクタの引数にPageParametersオブジェクトとして 渡されるので、パラメータ名を指定して値を取得する。 なお、toStringメソッドの引数は、パラメータが無い時の初期値。 Ajax Wicketにはサーバと通信して ページを部分的に更新するための Ajax機能が組み込まれている WicketでAjax機能を 利用する方法 • Ajaxを内蔵したコンポーネントを使う • Ajaxを内蔵したビヘイビアを使う ビヘイビア:コンポーネントにaddして使う後付け命令 Ajaxを利用した コンポーネントの例 • AjaxLink, AjaxButton リンクやボタンを押すとコンポーネントを更新 (AjaxButtonはFormの送信も) • IndicatingAjaxLink, IndicatingAjaxButton AjaxLink,AjaxButtonにインジケータ(ぐるぐる) を表示 • AjaxEditableLabel, AjaxEditableMultiLineLabel その場で編集できるラベル Ajaxを利用した コンポーネントの例 • AutoCompleteTextField 入力値を補完するテキストフィールド • LazyLoadPanel パネルが表示されるまでインジケータ(ぐるぐる)表示 • ModalWindow 画面内の子ウィンドウ • NestedTree ディレクトリツリー などなど AjaxLink new AjaxLink<Void>("link") { @Override public void onClick(AjaxRequestTarget target) { // AjaxLinkがクリックされた時の処理 } } リンクやボタン用のクリック時に Ajaxで処理をするコンポーネント 利用例 リンクをクリックするたびに 表示・非表示 ページのHTML の一部 <p><a wicket:id="link">リンク</a></p> <div wicket:id="green"><p>上のリンクを押すと表示が切り替わります</p></div> ページクラスの コンストラクタ final WebMarkupContainer green = new WebMarkupContainer("green") { @Override protected void onInitialize() { super.onInitialize(); // Ajaxの更新対象に設定し、さらに非表示の際も更新対象とする setOutputMarkupId(true); setOutputMarkupPlaceholderTag(true); } @Override protected void onConfigure() { super.onConfigure(); // コンポーネントを更新すると、現在の表示状態を反転する setVisible(!isVisible()); } }; add(green); add(new AjaxLink<Void>("link") { @Override public void onClick(AjaxRequestTarget target) { // green変数のコンポーネントを更新する target.add(green); } }); Ajaxの更新対象にする上 で重要なメソッド • Component#onInitialize() コンポーネントのインスタンス化の時に実行されるメソッド コンポーネントの初期設定などを記述する • Component#onConfigure() コンポーネントの更新のたびに実行されるメソッド コンポーネントの更新時にあわせた設定変更などを記述する • Component#setOutputMarkupId(bolean) コンポーネントをAjaxの更新対象にする(JavaScriptのフック用idを生成) • Component#setOutputMarkupPlaceholderTag(boolean) コンポーネントが非表示の時にもJavaScriptがフックできるようにタグを残す (非表示にならなければ不要) • AjaxRequestTarget#add(Component) 引数に与えられたComponentをAjaxで更新する LazyLoadPanel new AjaxLazyLoadPanel("id") { @Override public Component getLazyLoadComponent(String id) { // 表示されるPanelを生成する処理 } }; 表示されるPanelの準備ができるまで インジケータ(ぐるぐる)を表示する コンポーネント 利用例 内容が表示されるまで インジケータが代わりに表示される ページのHTML の一部 <div wicket:id="lazyLoad"></div> ページクラスのコンストラクタ add(new AjaxLazyLoadPanel("lazyLoad") { @Override public Component getLazyLoadComponent(String id) { return new DatePrintPanel(id); } }); パネルのHTML の一部 <wicket:panel> <p wicket:id="date"></p> </wicket:panel> パネルクラス public class DatePrintPanel extends Panel { public DatePrintPanel(String id) { super(id); add(new DateLabel("date", Model.of(new Date()), new PatternDateConverter("yyyy/MM/dd HH:mm:ss", true))); } } Ajaxを利用した ビヘイビアの例 • AbstractAjaxTimerBehavior AjaxSelfUpdatingTimerBehavior 定期的な処理の実施 • AjaxEventBehavior JavaScriptのイベント時に処理を実施 Ajaxを利用した ビヘイビアの例 • AjaxFormSubmitBehavior AjaxFormValidationBehavior Form用のComponentに対して、JavaScriptのイベン ト時にFormをSubmitした上での処理 • AjaxFormComponentUpdatingBehavior AjaxFormChoiceComponentUpdatingBehavior OnChangeAjaxBehavior Form用のComponentに対して、JavaScriptのイベン ト時にコンポーネントのModelを更新した上での処理 AjaxSelfUpdating TimerBehavior new AjaxSelfUpdatingTimerBehavior(Duration); 指定されたタイミング(Duration)ごとに、 コンポーネントを更新するビヘイビア add(new AjaxSelfUpdatingTimerBehavior(Duration.seconds(1))); add(new AjaxSelfUpdatingTimerBehavior(Duration.minutes(15))); 利用例 1秒ごとに現在時刻をカウントアップする ページのHTML の一部 <p>現在時刻:<span wicket:id="clock"></span></p> ページクラスのコンストラクタ IModel<Date> clockModel = new AbstractReadOnlyModel<Date>() { @Override public Date getObject() { return new Date(); } }; add(new DateLabel("clock", clockModel, new PatternDateConverter("yyyy/MM/dd HH:mm:ss", true)) { @Override protected void onInitialize() { super.onInitialize(); add(new AjaxSelfUpdatingTimerBehavior(Duration.seconds(1))); } }); AjaxFormComponent UpdatingBehavior new AjaxFormComponentUpdatingBehavior("JavaScriptイベント") { @Override protected void onUpdate(AjaxRequestTarget target) { // JavaScriptイベントが発生した時の処理 } }; JavaScriptイベントが発生したときに onUpdateメソッドの内容を実行するビヘイビア 利用例 選択されたものがAjaxで画面に表示される ページのHTML の一部 <form wicket:id="form"> <select wicket:id="dropDown"></select> <p>えらんだもの:<span wicket:id="choiced"></span></p> </form> ページクラスのコンストラクタ 途中まで // 選択肢のモデルを用意. IModel<List<String>> dropDownModel = new ListModel<>(Arrays.asList("札幌", "東京", "大阪", "福岡")); // 洗濯結果を取り扱うモデルを用意. IModel<String> choicedModel = new Model<String>(null); Form<Void> form = new Form<Void>("form"); add(form); final Label choiced = new Label("choiced", choicedModel) { @Override protected void onInitialize() { super.onInitialize(); setOutputMarkupId(true); } }; form.add(choiced); 次スライドへ ページクラスのコンストラクタ 続き // ドロップダウン形式のコンポーネントを用意. form.add(new DropDownChoice<String>("dropDown", choicedModel, dropDownModel) { @Override protected void onInitialize() { super.onInitialize(); add(new AjaxFormComponentUpdatingBehavior("change") { @Override protected void onUpdate(AjaxRequestTarget target) { // choiced 変数のコンポーネントを更新 target.add(choiced); } }); } }); Ajaxを利用した コンポーネント・ビヘイビアの例 公式のサンプル http://www.wicket-library.com/wicket-examples-6.0.x/ajax/ click! Ajax機能を利用しているページでは、 デバッグウィンドウで処理状況を確認できる アプリケーションをdeploymentモードで起動すると、 このウィンドウは表示されなくなる Session Wicketの内部ではSessionが使われている (例:ステートフル処理) 個人認証など独自のSession管理を行うには 1.独自のSessionクラスをWebSessionの サブクラスとして作成 2.WebApplicationのサブクラスで、独自 のSessionクラスを利用するように設定 独自のセッションクラスを作成(WebSessionクラスのサブクラスにする) public class WS01Session extends WebSession { public WS01Session(Request request) { super(request); } public static WS01Session get() { return (WS01Session) Session.get(); } } Sessionを取得するための static メソッドを必ず作成する! Wicketではページ(コンポーネントやモデル)の状態の保持にSessionを利用する。 そのため、変数にSessionを格納するとSessionが循環して参照されてしまう恐れがあ る(保持された場合は StackOverflowError が発生)。 このためWicketでSessionを利用する場合は変数に格納せず、上記の様な static メソ ッドを作った上で、使うたびごとに WS01Session.get(); のように実行する。 WebApplication クラスのサブクラスで、独自のSessionを利用するように設定 public class WS01Application extends WebApplication { // 中略 @Override public Session newSession(Request request, Response response) { // このアプリケーション用に独自に拡張したSessionを作成. return new WS01Session(request); } } 実例 サインアウト サインイン 未サインイン リダイレクト SimpleSignInPage.html の一部 <form wicket:id="form"> <label>ユーザID : </label> <input type="text" wicket:id="userId"> <label>パスフレーズ : </label> <input type="password" wicket:id="passphrase"> <br><button type="submit">サインイン</button> </form> SignedPage.html の一部 <p><span wicket:id="userId"></span>さんがサインインしました!</p> <p><a wicket:id="signOut">サインアウト</a></p> WS01Session.java 途中まで public class WS01Session extends WebSession { private boolean signed; private boolean userId; public WS01Session(Request request) { super(request); signed = false; userId = null; } public static WS01Session get() { return (WS01Session) Session.get(); } public boolean isSigned() { return signed; } 次スライドへ WS01Session.java つづき public boolean signIn(String userId, String passphrase) { if (userId != null && passphrase != null) { if (userId.equals(passphrase)) { replaceSession(); // Session Fixation対策 this.signed = true; this.userid = userId; } } return signed; } public void signOut() { invalidate(); } public String getUserId() { return userId != null ? userId : "不明"; } } Webアプリケーションのサブクラスへの設定は前述と同様のため、省略する。 SimpleSignInPage.java public class SimpleSignInPage extends WS01TemplatePage { private String userId; private String passphrase; public SimpleSignInPage() { // ページのフィールド変数とコンポーネントを関連づける CompoundPropertyModel を用意. // ログインなどのステートフルにしたくない部分には Stateless コンポーネントを利用. StatelessForm<SimpleSignInPage> form = new StatelessForm<SimpleSignInPage>("form", new CompoundPropertyModel<>(this)) { @Override protected void onSubmit() { super.onSubmit(); if (WS01Session.get()).signIn(userId, passphrase) { setResponsePage(SignedPage.class); } } }; add(form); form.add(new RequiredTextField<String>("userId"); form.add(new PasswordTextField("passphrase"); } } SignedPage.java public class SignedPage extends WS01TemplatePage { public SignedPage() { this.add(new Label("userId", WS01Session.get().getUserId())); // ログアウトなどのステートフルにしたくない部分には Stateless コンポーネントを利用. this.add(new StatelessLink<Void>("signOut") { @Override public void onClick() { WS01Session.get().signOut(); // サインアウトしたら強制的にログイン画面へ. throw new RestartResponseException(SimpleSignInPage.class); } }); } @Override protected void onInitialize() { super.onInitialize(); if (!WS01Session.get().isSigned()) { // ページの生成時にサインインしてない場合は強制的にログイン画面へ. throw new RestartResponseException(SimpleSignInPage.class); } } } より利便性の高い認証機構は Wicket-Auth-Rolesなどを利用 Apach Shiro も便利の様です 付録 Abstract ReadOnlyModel IModel<T> model = new AbstractReadOnlyModel<T>() { @Override public T getObject() { // コンポーネントから呼び出される度に実行する // オブジェクトの準備処理 } }; コンポーネントにデータを要求される度に オブジェクトを生成して返すModel DateLabel new DateLabel("id", IModel<Date>, パターン); 日付をパターンの通りに整形して表示する DateLabel dl = new DateLabel("clock", clockModel, new PatternDateConverter("yyyy/MM/dd HH:mm:ss", true); 表示形式は PatternDateConverter もしくは StyleDateConverter で設定する Panel <!DOCTYPE html> <html lang="ja" xmlns:wicket="http://wicket.apache.org"> <head> <meta charset="UTF-‐8" /> </head> <body> <wicket:panel> <!-‐-‐ ひとまとめにするコンポーネント用のタグ -‐-‐> </wicket:panel> </body> </html> SamplePage.html public class SamplePanel extends Panel { public SamplePanel(String id) { super(id); // ひとまとめにするコンポーネントの準備 } } SamplePage.java Panelコンポーネントは、複 数のコンポーネントをひとま とめにしたパーツとして扱う ことができる Pageの<div>タグなどに addすると、<wicket:panel> タグの中身がdiv要素の中身 としてページに反映される 複数回使い回すページ部分や、 NestedForm、 LazyLoadPanelを利用して遅延読込を させたい部分などに使う