技術校正:tkcn
這份文件說明了如何用 GWT 2.0 的 UiBinder,透過 XML 語法來建構 widget 跟 DOM 的結構。但不包括 binder 的 l10n 議題,那部份請讀 i18n - UiBinder。
概論本質上,一個 GWT application 是一個網頁。當你在作網頁版面設計時,用 HTML 跟 CSS 是很直覺的方式。「在 HTML 頁面上裝置許多 GWT widget 來建立 application」—UiBinder 這個 framework 可以讓你徹底做到這件事。
UiBinder 除了提供一個比寫 code 更自然且簡單的方法來建立 UI,也可以讓你的 application 更有效率。browser 在建立 DOM 結構時,把一大串 HTML 碼塞進
innerHTML 屬性,會比呼叫一堆 API 還好的多。UiBinder 自然就利用這一點,讓你可以用最快樂也最好的方式來建構 application、UiBinder 可以...
不過,當你在學習 UiBinder 的時候,也要瞭解哪些不是 UiBinder 的功能。UiBinder 不是 renderer(描繪者)。語法中也沒有迴圈、條件判斷。UiBinder 允許你佈置你的 widget,但 widget 仍然需要自己將資料轉成 HTML。
接下來,我們會透過一系列典型的使用狀況,來說明 UiBinder 的使用方式。你會看到如何作 UI 的排版、設計 style、以及掛上 event handler。i18n 的部份請讀 i18n - UiBinder。
快速開始:相反地,如果你想直接殺進程式碼,那看看這個版本。它把舊的 Mail 範例改成用 UiBinder。Mail.java 跟 Mail.ui.xml 要一起看。
萬年老梗:Hello World!先來一個 UiBinder template 的範例,內容非常解單,只有 HTML 沒有 widget:
<!-- HelloWorld.ui.xml --> <ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'> <div> Hello, <span ui:field='nameSpan'/>. </div> </ui:UiBinder> 現在假設一個狀況:你需要用程式存取
<span ui:field='nameSpan'> 的文字。你可能會喜歡實際寫 Java 程式碼來作這件事情,所以 UiBinder template 有一個對應的 owner class 可以讓程式存取 template 當中宣告的 UI 結構。上面這個 template 對應的 owner class 會長的像這樣:public class HelloWorld extends UIObject { // Could extend Widget instead interface MyUiBinder extends UiBinder<DivElement, HelloWorld> {} private static MyUiBinder uiBinder = GWT.create(MyUiBinder.class); @UiField SpanElement nameSpan; public HelloWorld() { // createAndBindUi initializes this.nameSpan setElement(uiBinder.createAndBindUi(this)); } public void setName(String name) { nameSpan.setInnerText(name); } } 接下來你可以在任何一段 UI 程式碼當中實體化並使用這個 owner class。下面這個例子直接使用 DOM 操作,讓你用 UiBinder 使用 widget:
HelloWorld helloWorld = new HelloWorld(); Document.get().getBody().appendChild(helloWorld.getElement()); helloWorld.setName("World"); UiBinder 是產生 UI 結構並且跟 Java 的 owner class 黏接在一起的工廠。程式碼當中的 interface:
UiBinder<U, O> 宣告了兩個參數型態:
所有在 ui.xml 檔案中宣告的東西(包括 DOM element),在 owning class 當中可以用 field 的名稱來操作。範例程式碼裡頭的
<span> 就把 ui:field 設成 nameSpan ,在 Java 程式碼當中用 @UiField 這個 annotation 來標記。當執行到 uiBinder.createAndBindUi(this) 時,指定的 field 就會填入適合的 SpanElement instance。我們建立的 class 是繼承 UiObject,不過也可以簡單地繼承
Widget 、 Composite 或 Object 就好,沒有特別限制。不過,請注意標有 @UiField 的 field 必須有 default(package )的可見度,如果它們要被 binder 使用到,就不能宣告成 private 。widget 版 Hello World下面是一個使用 widget 的 UiBinder template 例子:
<!-- HelloWidgetWorld.ui.xml --> <ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder' xmlns:g='urn:import:com.google.gwt.user.client.ui'> <g:HTMLPanel> Hello, <g:ListBox ui:field='listBox' visibleItemCount='1'/>. </g:HTMLPanel> </ui:UiBinder> public class HelloWidgetWorld extends Composite { interface MyUiBinder extends UiBinder<Widget, HelloWidgetWorld> {} private static MyUiBinder uiBinder = GWT.create(MyUiBinder.class); @UiField ListBox listBox; public HelloWidgetWorld(String... names) { // sets listBox initWidget(uiBinder.createAndBindUi(this)); for (String name : names) { listBox.addItem(name); } } } // Use: HelloWidgetWorld helloWorld = new HelloWidgetWorld("able", "baker", "charlie"); 我們不但用了 widget,也創造了一個 widget。
HelloWorldWidget 可以加到任何一個 panel 上頭去。為了在 ui.xml 當中用一堆 widget,你必須在 XML 的 namespace prefix 中註明 widget 的 package。這是為甚麼 root element
<ui:uibinder> 有 xmlns:g='urn:import:com.google.gwt.user.client.ui' 這個 attribute。如此設定之下,com.google.gwt.user.client.ui 這個 package 下的所有 class 都可以用 prefix 是 g、名稱跟 Java class 名稱一樣的 tag(例如 <g:ListBox> )來作為 XML template 的一個 element。g:ListBox 的 visibleItemCount='1' 這個 attribute 是什麼意思呢?它會呼叫 ListBox#setVisibleItemCount(int) 。widget 每個符合 JavaBean 格式的 method,都可以透過這個方法來設定 property。這邊特別注意
HTMLPanel instance 的使用。HTMLPanel 在混和 HTML 跟 widget 方面非常好用,UiBinder 搭配 HTMLPanel 使用起來也很順。一般來說,任何時候需要在 widget hierarchy 當中使用 HTML,你會需要一個 HTMLPanel 的 instance 或是 HTML 這個 widget。panel 的使用所有 panel(理論上來說,是所有 implement HasWidgets 的 class)可以用在 template 檔當中,也可以有其他 panel 在裡頭。
<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder' xmlns:g='urn:import:com.google.gwt.user.client.ui'> <g:HorizontalPanel> <g:Label>Keep your ducks</g:Label> <g:Label>in a row</g:Label> </g:HorizontalPanel> </ui:UiBinder> 某些既有的 GWT widget 需要特別的 XML 檔案設定,你會在 javadoc 當中看到。下面是
DockLayoutPanel 的設定:<g:DockLayoutPanel unit='EM'> <g:north size='5'> <g:Label>Top</g:Label> </g:north> <g:center> <g:Label>Body</g:Label> </g:center> <g:west size='192'> <g:HTML> <ul> <li>Sidebar</li> <li>Sidebar</li> <li>Sidebar</li> </ul> </g:HTML> </g:west> </g:DockLayoutPanel> 另外也要注意,在大多數的 panel 中,我們無法直接放 HTML 進去;但是在 HTMLPanel、或是 implement HasHTML 的 widget(例如
<g:west> 下頭的東西)。未來的版本可能會擺脫這個限制,但目前你必須將你的 HTML 放到 HTML-savvy widget 中。關於 HTML entityUiBinder 的 template 是 XML 檔,XML 並無法解讀像
的 entity。當你需要用到這些字時,你需要自己定義他們。為了方便起見,我們提供了一組定義,你可以透過設定 DOCTYPE 來引入:<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent"> GWT compiler 並不會真的連到這個 URL 去抓檔案,因為 compiler 已經有一份了。不過呢... 你的 IDE 可能會去抓......
簡化 event handler 的 bindingUiBinder 的其中一個目標是減少建立 UI 時那些單調乏味的 Java 程式碼、以及減少 讓千篇一律的 event handler 讓你腦袋麻木。這樣的程式碼你已經抄了幾次了?
public class MyFoo extends Composite { Button button = new Button(); public MyFoo() { button.addClickHandler(new ClickHandler() { public void onClick(ClickEvent event) { handleClick(); } }); initWidget(button); } void handleClick() { Window.alert("Hello, AJAX"); } } 在一個 UiBinder 的 owner class,你可以用
@UiHandler 這個 annotation 來使用程式碼當中的 anoymous class:public class MyFoo extends Composite { @UiField Button button; public MyFoo() { initWidget(button); } @UiHandler("button") void handleClick(ClickEvent e) { Window.alert("Hello, AJAX"); } } 不過,目前(至少這個版本)有個限制:你只能在 widget 產生的 event 使用
@UiHandler ,DOM element 則不行。也就是說,<g:Button> 可以用、<button> 就不行。掛上 CSS 吧!你可以直接在需要的地方,用
<ui:style> 來定義 CSS:<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'> <ui:style> .pretty { background-color: Skyblue; } </ui:style> <div class='{style.pretty}'> Hello, <span ui:field='nameSpan'/>. </div> </ui:UiBinder> CssResource 這個 interface 會伴隨 ClientBundle 而產生。這表示如果你拼錯所使用的 class 名稱(打成 {style.pretttttttty}),compiler 警告你。此外,你的 CSS class 名稱會被 obfuscate,如此可以防止其他 CSS 檔裡頭有相同的名稱—也就是說—不再有 global 的 CSS namespace 啦!事實上,在單一個 template 就可以享受到好處啦:
<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'> <ui:style> .pretty { background-color: Skyblue; } </ui:style> <ui:style field='otherStyle'> .pretty { background-color: Orange; } </ui:style> <div class='{style.pretty}'> Hello, <span class='{otherStyle.pretty}' ui:field='nameSpan'/>. </div> </ui:UiBinder> 還沒完咧~ 你也可以不用在
ui.xml 檔當中寫這些 CSS。大多數真正的 project 會把 CSS 檔分離出去。在下面的例子當中,src 的值是相對位置(相對於 ui.xml 檔)。<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'> <ui:style src="MyUi.css" /> <ui:style field='otherStyle' src="MyUiOtherStyle.css"> <div class='{style.pretty}'> Hello, <span class='{otherStyle.pretty}' ui:field='nameSpan'/>. </div> </ui:UiBinder> 用程式存取 CSS 的 style你的程式碼會需要存取 template 用到的某些樣式。舉例來說,假如你的 widget 需要依照 enable 或是 disable 的狀態改變顏色:
<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'> <ui:style type='com.my.app.MyFoo.MyStyle'> .redBox { background-color:pink; border: 1px solid red; } .enabled { color:black; } .disabled { color:gray; } </ui:style> <div class='{style.redBox} {style.enabled}'>I'm a red box widget.</div> </ui:UiBinder> public class MyFoo extends Widget { interface MyStyle extends CssResource { String enabled(); String disabled(); } @UiField MyStyle style; /* ... */ void setEnabled(boolean enabled) { getElement().addStyle(enabled ? : style.enabled() : style.disabled()); getElement().removeStyle(enabled ? : style.disabled() : style.enabled()); } } <ui:style> 有一個 attribute:type='com.my.app.MyFoo.MyStyle' 。這表示它必須 implement CssResource 這個 interface(在 Java 程式碼當中,定義在 MyFoo 之後),並且提供兩個會用到的 CSS 的 class:enabled 跟 disabled。 現在看到
MyFoo.java 的 @UiField MyStyle style; ,這讓程式可以去存取從 <ui:style> 產生出來的 CssResource 。setEnabled 這個 method用來根據 widget 是開啟或是關閉的狀態來設定 enabled 或是 disabled 的 style。你可以隨心所欲地在指定的 style 區塊當中定義一堆 class,但是你的程式只能存取 interface 有涵蓋的到的 style。
使用外部 resource有時候你的 template 會需要用到一些本身沒有定義、外來的 style 或是其他 object。
<ui:with> 可以做到這件事:<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder' xmlns:g='urn:import:com.google.gwt.user.client.ui'> <ui:with field='res' type='com.my.app.widgets.logoname.Resources'/> <g:HTMLPanel> <g:Image resource='{res.logoImage}'> <div class='{res.style.mainBlock}'> <div class='{res.style.userPictureSprite}' /> <div> Well hello there <span class='{res.style.nameSpan}' ui:field='nameSpan'/> </div> </div> </g:HTMLPanel> </ui:UiBinder> /** * Resources used by the entire application. */ public interface Resources extends ClientBundle { @Source("Style.css") Style style(); @Source("Logo.jpg") ImageResource logo(); public interface Style extends CssResource { String mainBlock(); String nameSpan(); Sprite userPictureSprite(); } } with 這個 element 宣告了一個 field,這個 field 是一個 resource object,而它的 attribute 會呼叫對應的 method。在這個範例當中,它會呼叫 GWT.create(Resource.class) 來建立 instance。(請繼續讀下去,瞭解它是如何傳遞一個 instance、而不是擁有一個你創造的 instance)請注意,ui:with 用到的 resource 並不需要 implement ClientBundle 這個 interface,這裡只是方便舉例。
共用 resource 的 instances你可以用
<ui:with> 讓 template 可以使用到 resource,但是代價就是你必須自己建立 instance。相反地,如果你希望你的程式碼負責尋找或建立該 resource,你有兩種方法來做到:你可以用 @UiFactory 來標記一個 factory method、或是自己填入該 field 然後加上 @UiField(provided=true) 的 annotation。我們修改前面的範例,來展示使用 @UiFactory 來提供 template 當中需要用到
Resources 的 instance。public class LogoNamePanel extends Composite { interface MyUiBinder extend UiBinder<Widget, LogoNamePanel> {} private static MyUiBinder uiBinder = GWT.create(MyUiBinder.class); @UiField SpanElement nameSpan; final Resources resources; public LogoNamePanel(Resources resources) { this.resources = resources; initWidget(uiBinder.createAndBindUi(this)); } public void setUserName(String userName) { nameSpan.setInnerText(userName); } @UiFactory /* this method could be static if you like */ public Resources getResources() { return resources; } } 所有 template 當中是
Resources 的 field 會呼叫 getResource 來建立 instance。但是在這個例子當中,只有一個這樣的 field。你可以用 @UiField(provided=true) 來讓它們更簡潔。public class LogoNamePanel extends Composite { interface MyUiBinder extends UiBinder<Widget, LogoNamePanel> {} private static MyUiBinder uiBinder = GWT.create(MyUiBinder.class); @UiField SpanElement nameSpan; @UiField(provided = true) final Resources resources; public LogoNamePanel(Resources resources) { this.resources = resources; initWidget(uiBinder.createAndBindUi(this)); } public void setUserName(String userName) { nameSpan.setInnerText(userName); } } 需要在 constructor 傳入參數的 widget...每個在 template 宣告的 widget 會呼叫
GWT.create() 來建造。在大多數的狀況下,這代表它們必須用預設的方法建立 instance—也就是說,它們必須提供一個沒有參數的 constructor。不過,也有一些方法來解決這個問題。除了前面提到 @UiFactory 跟 @UiField(provided=true) 的語法外,你可以把你的 widget 掛上 @UiConstructor 這個 annotation。假設你有一個 widget 的 constructor 長這樣:
public CricketScores(String... teamNames) {...} 在 template 要用到它:
<!-- UserDashboard.ui.xml --> <ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder' xmlns:g='urn:import:com.google.gwt.user.client.ui' xmlns:my='urn:import:com.my.app.widgets' > <g:HTMLPanel> <my:WeatherReport ui:field='weather'/> <my:Stocks ui:field='stocks'/> <my:CricketScores ui:field='scores' /> </g:HTMLPanel> </ui:UiBinder> public class UserDashboard extends Composite { interface MyUiBinder extends UiBinder<Widget, UserDashboard> {} private static MyUiBinder uiBinder = GWT.create(MyUiBinder.class); public UserDashboard() { initWidget(uiBinder.createAndBindUi(this)); } } 就會有錯誤訊息跑出來:
[ERROR] com.my.app.widgets.CricketScores has no default (zero args) constructor. To fix this, you can define a @UiFactory method on the UiBinder's owner, or annotate a constructor of CricketScores with @UiConstructor. 所以,要嘛使用
@UiFactory :public class UserDashboard extends Composite { interface MyUiBinder extends UiBinder<Widget, UserDashboard>; private static final MyUiBinder uiBinder = GWT.create(MyUiBinder.class); private final String[] teamNames; public UserDashboard(String... teamNames) { this.teamNames = teamNames; initWidget(uiBinder.createAndBindUi(this)); } /** Used by MyUiBinder to instantiate CricketScores */ @UiFactory CricketScores makeCricketScores() { // method name is insignificant return new CricketScores(teamNames); } } public @UiConstructor CricketScores(String teamNames) { this(teamNames.split("[, ]+")); } <!-- UserDashboard.ui.xml --> <g:HTMLPanel xmlns:ui='urn:ui:com.google.gwt.uibinder' xmlns:g='urn:import:com.google.gwt.user.client.ui' xmlns:my='urn:import:com.my.app.widgets' > <my:WeatherReport ui:field='weather'/> <my:Stocks ui:field='stocks'/> <my:CricketScores ui:field='scores' teamNames='AUS, SAF, WA, QLD, VIC'/> </g:HTMLPanel> @UiField(provided=true) 來代入:public class UserDashboard extends Composite { interface MyUiBinder extends UiBinder<Widget, UserDashboard>; private static final MyUiBinder uiBinder = GWT.create(MyUiBinder.class); @UiField(provided=true) final CricketScores cricketScores; // cannot be private public UserDashboard(CricketScores cricketScores) { // DI fans take note! this.cricketScores = cricketScores; initWidget(uiBinder.createAndBindUi(this)); } } 在不同 XML template 檔中使用相同 widget你是一個 MVP的開發人員,已經有一個很好的 view interface 以及 implement 後的樣板 widget。要如何在許多不同的 XML template 用同一個 view 呢?
誠實的警告: 這只是為了示範在不同的
ui.xml 檔使用相同程式碼。在實作上,它不是一個成熟的 pattern,也許不是最好的方法。public class FooPickerController { public interface Display { HasText getTitleField(); SourcesChangeEvents getPickerSelect(); } public void setDisplay(FooPickerDisplay display) { ... } } public class FooPickerDisplay extends Composite implements FooPickerController.Display { @UiTemplate("RedFooPicker.ui.xml") interface RedBinder extends UiBinder<Widget, FooPickerDisplay> {} private static RedBinder redBinder = GWT.create(RedBinder.class); @UiTemplate("BlueFooPicker.ui.xml") interface BlueBinder extends UiBinder<Widget, FooPickerDisplay> {} private static BlueBinder blueBinder = GWT.create(BlueBinder.class); @UiField HasText titleField; @UiField SourcesChangeEvents pickerSelect; public HasText getTitleField() { return titleField; } public SourcesChangeEvents getPickerSelect() { return pickerSelect; } protected FooPickerDisplay(UiBinder<Widget, FooPickerDisplay> binder) { initWidget(uiBinder.createAndBindUi(this)); } public static FooPickerDisplay createRedPicker() { return new FooPickerDisplay(redBinder); } public static FooPickerDisplay createBluePicker() { return new FooPickerDisplay(blueBinder); } } |