Wicket Examples - forminput編 -

「サンプルのソースを見る」の第5回forminput編。このサンプルでは、

  • いろいろなフォーム要素の使い方

がなんとなくわかります(^_^;)。

アプリケーションクラス(FormInputApplication.java)

まずは、アプリケーションクラスです。こちらは、ホームページクラスを返しているだけなので、割愛します・・・といきたいところですが、このクラスでは、initメソッドをオーバーライドして処理を追加しています。

まずは、例外処理設定のインスタンスを取得して、指定したりソースが見つからなかった場合でも例外をスローしないように設定します。

		getExceptionSettings().setThrowExceptionOnMissingResource(false);

次に、各言語用のボタンの設定を行います。ここでは日本語の設定を見てみます。まず、Fontのインスタンス生成します。次に、ボタンイメージ(DefaultButtonImageResourceクラス)のインスタンスを生成し、先ほど生成したFontインスタンスを設定します。DefaultButtonImageResourceクラスのコンストラクタには、ボタンのラベル、ここでは"保存"を指定しています。

		Font fontJa = new Font("Serif", Font.BOLD, 16);
		DefaultButtonImageResource imgSaveJa = new DefaultButtonImageResource("\u4fdd\u5b58");
		imgSaveJa.setFont(fontJa);

次に、共有リソース(SharedResourcesクラスのインスタンス)を取得し、先ほど生成したボタンイメージを追加します。addメソッドの第1引数には、リソースの論理名を指定しますが、これは各言語共通みたいです。

		SharedResources sharedResources = getSharedResources();
		sharedResources.add("save", Locale.JAPANESE, imgSaveJa);

Pageクラス(FormInput.java)

次に、Pageクラスです。コンストラクタでは、FeedbackPanelというコンポーネントと、Formコンポーネントを自分で拡張したコンポーネントを設定しています。FeedbackPanelコンポーネントは、メッセージを表示するさせることが出来るパネル、みたいです。このサンプルでは、"保存"ボタンを押下した際に、入力された内容が表示されます。

	public FormInput()
	{
		final FeedbackPanel feedback = new FeedbackPanel("feedback");
		add(feedback);
		add(new InputForm("inputForm"));
	}

次に、InputFormクラスです。Pageクラスの内部クラスで、Formコンポーネントを拡張しています。

	private class InputForm extends Form

コンストラクタでは、まずスーパークラスのコンストラクタを呼んでいます。Modelとして、CompoundPropertyModelを使用しています。CompoundPropertyModelクラスのコンストラクタには、Modelに結びつくBeanを設定しますが、今までのサンプルではPageクラスを設定していましたが、このサンプルでは、別クラスを設定しています。

		public InputForm(String name)
		{
			super(name, new CompoundPropertyModel(new FormInputModel()));

Formにコンポーネントを追加していきます。まずは、ロケールを選択するドロップダウンリストです。

			add(new LocaleDropDownChoice("localeSelect"));

次に、デフォルトロケールに表示を切り替えるためのハイパーリンクコンポーネントを追加しています。ハイパーリンクがクリックされた時に呼ばれるonClickメソッドをオーバーライドしています。onClickでは、リクエストからロケールを取得し、セッションに設定しています(setLocaleって、FormInput#setLocaleなんですかねぇ?)。

			add(new Link("defaultLocaleLink")
			{
				public void onClick()
				{
					WebRequest request = (WebRequest)getRequest();
					setLocale(request.getLocale());
				}
			});

次に、テキストフィールドをFormに追加します。入力必須としたいため、RequiredTextFieldコンポーネントを使用します。このコンポーネントは、TextFieldコンポーネントにRequiredValidatorというバリデータが付加されたコンポーネント、ということができます。入力されなかった場合は、バリデーションのエラーとなるのですが、そのエラーメッセージ中のラベルをRequiredTextField#setLabelで設定しているようです。設定しないと、RequiredTextFieldのコンストラクタで指定したidが使用されるようです。

			RequiredTextField stringTextField = new RequiredTextField("stringProperty");
			stringTextField.setLabel(new Model("String"));
			add(stringTextField);

次も、RequiredTextFieldコンポーネントですが、コンストラクタの第2引数にInteger.classを指定することで、入力内容がIntegerかどうかのバリデーションも動作するようになります。加えて範囲を0以上に制限しています。よって、全角で"−100"や"-1"を入力すると、バリデーションエラーとなります。

			RequiredTextField integerTextField = new RequiredTextField("integerProperty",
					Integer.class);
			add(integerTextField.add(NumberValidator.POSITIVE));

次も、RequiredTextFieldコンポーネントですが、入力内容がDoubleかどうかのバリデーションも動作します。

			add(new RequiredTextField("doubleProperty", Double.class));

次は日付の入力です。まず、表示用のラベルとしてWebMarkupContainerクラスを使用していますが、「何故?」という感じです... orz TextFieldコンポーネントを使用していますので、入力の省略が可能です。コンストラクタの第2引数にDate.classを指定していますので、Date型として不適切な入力はエラーとなります。あと、Wicket Extensionで提供されているDatePickerを使い、日付を入力しやすいようにしています。この時、DatePickerで選択した日付がテキストフィールドに反映されるように、DatePickerのコンストラクタに、ラベルのコンポーネントとテキストフィールドのコンポーネントを指定しているようです。

			WebMarkupContainer dateLabel = new WebMarkupContainer("dateLabel");
			add(dateLabel);
			TextField datePropertyTextField = new TextField("dateProperty", Date.class);
			add(datePropertyTextField);
			add(new DatePicker("datePicker", dateLabel, datePropertyTextField));

次は、0から100までIntegerが入力できるテキストフィールドです。NumberValidator.rangeメソッドで、チェック範囲を持つValidatorを生成するようです。

			add(new RequiredTextField("integerInRangeProperty", Integer.class).add(NumberValidator
					.range(0, 100)));

次は、単独のチェックボックスです。CheckBoxコンポーネントを使用します。

			add(new CheckBox("booleanProperty"));

次は、ラジオボタンです。RadioChoiceコンポーネントを使用します。RadioChoiceのコンストラクタに、各ラジオボタンにつけるラベルのリストを設定します。また、RadioChoice#setSuffixでラジオボタン間につけるセパレータを設定します。また、エラーメッセージ中のラベルを設定し、選択必須としています。

			RadioChoice rc = new RadioChoice("numberRadioChoice", NUMBERS).setSuffix("");
			rc.setLabel(new Model("number"));
			rc.setRequired(true);
			add(rc);

次も、ラジオボタンです。RadioChoiceコンポーネントとは違う方法です。最初に、RadioGroupコンポーネントで、ラジオボタンをグループ化するインスタンスを生成します。次に、各ラジオボタン用に、ListViewインスタンスを生成します。ListView#populateItemをオーバーライドし、ラジオボタンごとにRadioコンポーネントとLabelコンポーネントを設定しているようです。

			RadioGroup group = new RadioGroup("numbersGroup");
			add(group);
			ListView persons = new ListView("numbers", NUMBERS)
			{
				protected void populateItem(ListItem item)
				{
					item.add(new Radio("radio", item.getModel()));
					item.add(new Label("number", item.getModelObjectAsString()));
				};
			};
			group.add(persons);

次は、複数選択のチェックボックスです。基本的に、RadioGroupを使ったラジオボタンのときと同じです。CheckGroupコンポーネントで、チェックボックスをグループ化するインスタンスを生成します。次に、各チェックボックス用に、ListViewインスタンスを生成します。ListView#populateItemをオーバーライドし、チェックボックスごとにCheckコンポーネントとLabelコンポーネントを設定しているようです。

			CheckGroup checks = new CheckGroup("numbersCheckGroup");
			add(checks);
			ListView checksList = new ListView("numbers", NUMBERS)
			{
				protected void populateItem(ListItem item)
				{
					item.add(new Check("check", item.getModel()));
					item.add(new Label("number", item.getModelObjectAsString()));
				};
			};
			checks.add(checksList);

次は、複数選択可能なリストボックスです。ListMultipleChoiceコンポーネントを使用します。

			add(new ListMultipleChoice("siteSelection", SITES));

次も、テキストフィールドなわけですが、今までのStringやInteger型ではない型を使用する場合、Stringからある型へ、ある型からStringへ、相互変換するコンバータを用意します。ここでは、TextFieldのコンストラクタの第2引数にURL.classを指定し、コンバータを返すgetConverterをオーバーライドします。コンバータ自体は、SimpleConverterAdapterを拡張し、ある型から文字列に変換するtoStringメソッドと、文字列からある型に変換するtoObjectをオーバーライドします。

			add(new TextField("urlProperty", URL.class)
			{
				public IConverter getConverter()
				{
					return new SimpleConverterAdapter()
					{
						public String toString(Object value)
						{
							return value != null ? value.toString() : null;
						}

						public Object toObject(String value)
						{
							try
							{
								return new URL(value.toString());
							}
							catch (MalformedURLException e)
							{
								throw new ConversionException("'" + value + "' is not a valid URL");
							}
						}
					};
				}
			});

次もテキストフィールドなわけですが、今度はUsPhoneNumberという自作クラスを型として使用する場合です。ここでもgetConverterメソッドをオーバーライドしコンバータを返しているのですが、ここではMaskConverterという、マスクパターンを使用して変換を行うコンバータを使用しています。

			add(new TextField("phoneNumberUS", UsPhoneNumber.class)
			{
				public IConverter getConverter()
				{
					return new MaskConverter("(###) ###-####", UsPhoneNumber.class);
				}
			});

次に、ListViewクラスを拡張したクラスを追加しています。ListViewクラス自体、よくわかっていませんが... orz

			add(new LinesListView("lines"));

ここまでで入力用のコンポーネントは終わりで、あとは"保存""リセット"のボタンになります。まずは、"保存"用のイメージボタンを追加します。ImageButtonコンポーネントを使用しています。

			add(new ImageButton("saveButton"));

次に、"リセット"用のイメージボタンです。LinkコンポーネントとImageコンポーネントを組み合わせて実現しています。これで、入力内容のリセットが出来るようですが、1) "InputForm.this"って何を指しているの? 2) modelChangedメソッド呼び出しでどうやってリセットされるの?、と不明点だらけです... orz

			add(new Link("resetButtonLink")
			{
				public void onClick()
				{
					InputForm.this.modelChanged();
				}
			}.add(new Image("resetButtonImage")));
		}

やっと、InputFormのコンストラクタが終わりました(^_^;) 次は、"保存"ボタンが押下された時に呼ばれるInputForm#onSubmitメソッドのオーバーライドです。バリデーションエラーにならなかった場合に実行されるようで、FeedbackPanelに、Modelに設定されているインスタンス(FormInputModelのインスタンス)のtoStringメソッドを実行した結果を、infoメッセージとして登録するようです(FeedbackPanelにメッセージが表示されます)。

		public void onSubmit()
		{
			info("Saved model " + getModelObject());
		}
	}

次は、Localeを選択するドロップダウンリスト用のコンポーネントです。DropDownChoiceを拡張しています。

	private final class LocaleDropDownChoice extends DropDownChoice
	{

LocaleDropDownChoiceクラスのコンストラクタでは、まずスーパークラスのコンストラクタを呼んでいます。この時、ドロップダウンリスト表示用のRendererクラスのインスタンスを設定しています。次にModelを設定しているわけですが、FormInputインスタンスのlocaleプロパティと結びつけるようです。でも、FormInputクラスにはsetLocaleしかないのですが、いいんですかねぇ?

		public LocaleDropDownChoice(String id)
		{
			super(id, LOCALES, new LocaleChoiceRenderer());
			setModel(new PropertyModel(FormInput.this, "locale"));
		}

次に、ドロップダウンリストの選択項目が変更された場合に、DropDownChoice#onSelectionChangedが呼ばれるようにするかどうかを指定します。デフォルトはfalseのようで、ここではtrueを指定してDropDownChoice#onSelectionChangedが呼ばれるようにしています。

		protected boolean wantOnSelectionChangedNotifications()
		{
			return true;
		}

そのonSelectionChangedメソッドですが、空実装です(^_^;) PropertyModelを使っていますので、FormInput#setLocaleが呼ばれて変更内容が反映されますので、ここでは何もしていないみたいです。

		public void onSelectionChanged(Object newSelection)
		{
		}
	}

ドロップダウンリストの表示内容をレンダリングするクラスです。

	private final class LocaleChoiceRenderer extends ChoiceRenderer
	{

ドロップダウンリストの各表示項目を返します。返したいのは、現在のロケールでの、各ロケールの表示名です。現在のロケールがLocal.JAPANESEなら"日本語"、Locale.ENGLISHなら"Japanese"、といった具合です。

		public Object getDisplayValue(Object object)
		{
			Locale locale = (Locale)object;
			String display = locale.getDisplayName(getLocale());
			return display;
		}
	}

さて、最後の内部クラスです。

	private static final class LinesListView extends ListView
	{

コンストラクタでは、setReuseItemsメソッドを呼んでいます。フォーム中で使用する場合は、設定する必要があるようです。

		public LinesListView(String id)
		{
			super(id);
			setReuseItems(true);
		}

各項目ごとにテキストフィールドを用意します。PropertyModelのコンストラクタの第1引数は、item.getModel()の戻り値は、FormInputModel.Lineになります。FormInputModel.Lineのプロパティtextにアクセスします。

		protected void populateItem(ListItem item)
		{
			item.add(new TextField("lineEdit", new PropertyModel(item.getModel(), "text")));
		}
	}

モデルオブジェクトクラス(FormInputModel.java)

次は、モデルオブジェクトクラスです。Serializableを実装するのは必須、みたいで。基本的に、Beanですね(^_^;)

public final class FormInputModel implements Serializable
{

フォーム中で使用するListView用のクラスです。textプロパティのみを持ちます。

	public final class Line implements Serializable
	{

このクラスでは、toStringメソッドをオーバーライドしています。toStringメソッドでは、プロパティに設定されている値を表示するようになっています。この値が"保存"ボタンが押下された場合に表示される値になります。

HTMLテンプレートファイル(FormInput_ja.html)

HTMLテンプレートファイルは、ロケールごとに用意されており、Javaのプロパティファイルと同様の命名規則になります。

ハイパーリンクは、こんな感じです。

		<a href="#" wicket:id="defaultLocaleLink">[default]</a>

日付選択のコンポーネントも、こんな感じ。

			  <span wicket:id="datePicker"></span>

フォーム中のListViewコンポーネントも、こんな感じ。

			  <ul>
			   <li wicket:id="lines">
			    <input type="text" wicket:id="lineEdit" /> 
			   </li>
			  </ul>

単独のチェックボックスは、こんな感じ。

			  <input wicket:id="booleanProperty" id="booleanProperty" type="checkbox"/>

ラジオボタンはこんな感じ。2通りのやり方がある、みたいです。

			  <span valign="top" wicket:id="numberRadioChoice" id="numberRadioChoice">
	    		<input type="radio">foo</input>
				<input type="radio">bar</input>
			  </span>

			  <span wicket:id="numbersGroup">
				<span wicket:id="numbers">
					<input type="radio" wicket:id="radio"/>
					<span wicket:id="number">[this is where number will be]</span>
				</span>
			  </span>

複数選択のチェックボックスは、こんな感じです。

			  <span wicket:id="numbersCheckGroup">
				<span wicket:id="numbers">
					<input type="checkbox" wicket:id="check"/>
					<span wicket:id="number">[this is where number will be]</span>
				</span>
			  </span>

複数選択のリストボックスは、こんな感じです。

			  <select wicket:id="siteSelection" id="siteSelection">
			  	<option>foo</option>
			  	<option>bar</option>
			  </select>

"保存"のイメージボタン、"リセット"のハイパーリンク+イメージボタンは、こんな感じです。

			  <input wicket:id="saveButton" type="image" value="buttonFactory:save:Save"/>
			  <a wicket:id="resetButtonLink" src="">
			      <img wicket:id="resetButtonImage" value="buttonFactory:reset:Reset"/> 
			  </a>

バリデーションエラーメッセージを表示したり、モデルオブジェクトの内容を表示したりするための、FeedbackPanalは、こんな感じです。div要素とspan要素の違いがよくわかっていませんが(^_^;)

	<div id="feedbackPanel">
		<span wicket:id="feedback"/>
    </div>

プロパティファイル(FormInput_ja.properties)

さて、最後のファイルとなる、プロパティファイルです。このファイルを何に使うかと言いますと、バリデーションエラーメッセージをロケールに合わせて表示する時に使用する、みたいです。

形式としては、[フォームのwicket:id] "." [フィールドのwicket:id] "." "TypeValidator"、みたいです。で、入力内容は、${input}、に埋め込まれるみたいです。

inputForm.urlProperty.TypeValidator='${input}' \u306f\u6b63\u3057\u3044URL\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002
inputForm.phoneNumberUS.TypeValidator='${input}' \u306f\u6b63\u3057\u3044\u96fb\u8a71\u756a\u53f7\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002