這篇是將 Google Web Toolkit 的官方教學文章直接翻譯過來的,內容不是很好閱讀,加減做個記錄。
GWT 的 Editor 架構(framework)可以讓複雜的物件(object graph)中的資料對應到 Editors 上,像是經由 RPC 得到的物件,如果要將其中的資料放到使用者介面(UI)上,就很適合用這個架構。
目的(Goals)
使用這個 Editor 架構主要的目的有下列幾項:
- 降低「膠水程式碼」(glue code)的使用量。(註一)
- 讓程式與 bean 物件相容,不管是 POJO、JSO、RPC 或是 RequestFactory,只要像是 bean 的物件(註二),都可以相容。
- 支援任意組合的 Editors。
註一:在一般的程式中,資料要在使用者介面與 object graph 之間互相傳遞時,都會需要撰寫一些程式碼,專門負責傳遞資料,這些很無聊但又不能省略的程式碼,就是所謂的「膠水程式碼」,使用 Editor 架構最主要的目的就是簡化這些程式碼的使用量。
註二:所謂「像是 bean 的物件」指的是物件中的 properties 都可以用 Foo getFoo();
這樣的方法(method)來取得,而 void setFoo(Foo foo);
則可有可無。
Editor 指的是一個可以編輯 bean 的物件,而一個 Editor 中可以包含任意個數的子 Editors,大多數的 Editor 會實作成 Widget,但在這個架構的規範數,並沒有這個限制,程式設計者可以任意實作出自己想要的 Editor 型式。
Driver 是在最上層的控制元件,負責將 bean 連結上 Editor。
Adapter:One of a number of provided types that provide “canned” behaviors for the Editor framework.
使用方式
這裡首先將 Editor 的架構作一個簡單的介紹,先讓大家有個概念,一般來說使用 Editor 架構大致包含幾個步驟:
- 實作與初始化 Editor。
- 實作與初始化 Driver。
- 將 bean 傳入 Driver 開始進行資料的編輯。
- 讓使用者與 UI 互動。
- 呼叫 Driver 的
flush()
函數將 Editor 中的資料複製回 bean。 - 透過
hasErrors()
與getErrors()
兩個函數檢查輸入的資料是否有錯誤。
以下是一個典型的實作流程:
Step 1
在 gwt.xml
檔中匯入 com.google.gwt.editor.Editor
這個模組。
Step 2
定義一般真正儲存資料的 POJO。
public class Person { Address getAddress(); Person getManager(); String getName(); void setManager(Person manager); void setName(String name); }
Step 3
依照自己的資料結構定義 Editor。
public class PersonEditor extends Dialog implements Editor<Person> { // 許多 GWT Widgets 都有支援 Editor 架構 Label nameEditor; // 建立 Editors 常常只是把既有的 Editors 組合一下而已 AddressEditor addressEditor; ManagerSelector managerEditor; public PersonEditor() { // 實作自己的 Widgets,這裡通常會使用 UiBinder } }
Step 4
接著就把 Editor 加入程式中。
public class EditPersonWorkflow{ // 宣告空的 interface,與 UiBinder 類似 interface Driver extends SimpleBeanEditorDriver<Person, PersonEditor> {} // 建立 Driver Driver driver = GWT.create(Driver.class); void edit(Person p) { // PersonEditor 是一個實作 Editor<Person> 的 DialogBox PersonEditor editor = new PersonEditor(); // 使用頂層的 editor 初始化 driver driver.initialize(editor); // 將物件中的資料複製到 UI 上 driver.edit(p); // 顯示 UI 到螢幕上 editor.center(); } // 這個函數可被其他的 UI 呼叫 void save() { Person edited = driver.flush(); if (driver.hasErrors()) { // 子 editor 回報錯誤 } doSomethingWithEditedPerson(edited); } }
以上就是 Editor 架構的典型實作方式。
實作 Editor 的規範
Editor 實際上只是一個標記參數的介面(interface)而已,其指定物件中的每個 property 所對應的子 Editor,而指定方式有下面幾種:
最簡單的方式就是要編輯的以物件 property 名稱作為子 Editor 的 field 名稱,或是 property 名稱再加上「Editor」,即 propertyNameEditor
,例如:
class MyEditor implements Editor<Foo> { // 編輯 Foo.getBar() property BarEditor bar; // 編輯 Foo.getBaz() property BazEditor bazEditor; }
其中 BarEditor
與 BazEditor
都是子 Editor 類別,而其 field 名稱則是對應 property 的名稱(即 bar
)或是加上「Editor」(即 bazEditor
)。
第二種方式是以物件 property 名稱或是 property 名稱再加上「Editor」(即 propertyNameEditor
)來定義沒有參數的方法函數,這樣的方式適合用於階層式的 Editor,例如:
interface FooEditor extends Editor<Foo> { // 編輯 Foo.getBar() property BarEditor bar(); // 編輯 Foo.getBaz() property BazEditor bazEditor(); }
最後一種是使用 @Path
語法,這個語法可以使用句點分隔的 property 路徑,標示更複雜的資料結構,可用於一般的 field 或 accessor 方法函數,例如:
class PersonEditor implements Editor<Person> { // 對應到 person.getManager().getName() @Path("manager.name"); Label managerName; }
@Ignored
語法可以讓 Editor 架構忽略指定的項目,不要視為子 Editor。
子 Editor 可以是 null,這種狀況 Editor 架構會自動忽略這些子 Editors。
在所有 Editor<t>
所使用的地方都可以用 IsEditor<editor>>
來替代,透過 IsEditor
介面可以讓你輕易的結合既有的 Editors 來創造新的 Editor,例如大部分的 leaf GWT Widget 都有實作 IsEditor
介面,因此可以直接拿來組合後建立新的 Editor。至於如果要自己實作 IsEditor
介面的話,只要實作 asEditor()
這個方法即可,不會影響到其他的程式碼。
Editor delegates
每個 Editor
都有一個 EditorDelegate
,提供 Editor 架構相關的 service 函數:
getPath()
會傳回目前 Editor 在 Editor 階層架構中的路徑。recordError()
會讓 Editor 將輸入驗證的錯誤訊息傳回它的上層 Editors(parent Editors),最後傳到 Driver 中。使用userData
參數可以將任意的資料放進產生的EditorError
中一起傳回。subscribe()
可以用來接收在 Editor 之外更新被編輯物件的訊息。
Editor subtypes
除了 Editor
這個介面(interface)之外,在 Editor 架構中還有一些特定的介面可以提供更複雜的功能。
LeafValueEditor
LeafValueEditor
適用在非物件、不可變動(immutable)或是任何不要讓 Editor 架構繼續往下解析的的型別。
setValue()
:設定要被編輯的物件。getValue()
:這個方法會在 Driver 呼叫flush()
時被呼叫,然後將 Editor 中的值複製到被編輯的 bean 之中。
HasEditorDelegate
HasEditorDelegate
提供一個具有 EditorDelegate
的 Editor。
setEditorDelegate()
:這個方法會在任何內容初始化動作之前被呼叫。
ValueAwareEditor
ValueAwareEditor
適用在 Editor 的動作會與其編輯的值相關的的狀況,也就是在其編輯的值變動時,會需要通知這個 Editor 做一些處理的動作。
setEditorDelegate()
:因為ValueAwareEditor
有繼承HasEditorDelegate
,所以要實作這個方法。setValue()
:設定要編輯與監控的物件,當此物間變動時就會通知 Editor。- 在任何的時候,當
EditorDelegate.subscribe()
被呼叫時,Editor 的onPropertyChange()
或setValue()
也會隨著被呼叫。 flush()
在被 Driver 呼叫時會以 depth-first 的方式處理子 Editor 中的flush()
,所以 Editors 通常不需呼叫子 Editors 的flush()
,而 Editor 如果要更換監控的物件,應該要在flush()
被呼叫之後才可更換,這樣可以讓編輯的流程可以被取消。
CompositeEditor
CompositeEditor
允許任何數量的子 Editors 在執行時動態加入,除了 ValueAwareEditor
所擁有的方法之外,CompositeEditor
還多了以下幾個方法:
createEditorForTraversal()
:呼叫此方法會傳回一個用來計算所有編輯路徑的子 Editor,如果這個 Composite Editor 正在編輯一個 Collection,這個方式可以解決沒有任何子 Editor 可以解查空 Collection 的問題。setEditorChain()
:提供一個可以設定EditorChain
的管道,讓子 Editors 可以放進 Editor 階層架構中或是從中移除。getPathElement()
:這個方法是提供給 Editor 架構用來計算每個子 Editor 的EditorDelegate.getPath()
傳回值用的,如果一個CompositeEditor
正在編輯有索引的資料結構(例如List
),則此方法就會傳回[index]
。
HasEditorErrors
HasEditorErrors
是表示這個 Editor 可以透過 EditorDelegate.recordError()
接收任何由子 Editor 所產生且未被處理完(unconsumed)的錯誤,而 Editor 可以將已經處理完的 EditorError
錯誤以 EditorError.setConsumed()
標示為處理完成。
各種 Adapters
GWT 提供了幾個 Editor adapter 類別,簡化一些常用的程式設計結構,大多數的 adapter 都有 of() 這個靜態方法可以直接建立 adapter 物件。
HasDataEditor
可以將List<T>
轉為HasData<T>
。HasTextEditor
將HasText
介面轉為LeafValueEditor<String>
。- 新的 widgets 建議以
TakesValue<String>
取代HasText
。 ListEditor
會維護一個List<T>
與子 Editors 所組成的List
之間的同步。ListEditor
在建立時會傳入一個使用者提供的EditorSource
,而這個EditorSource
會提供子 Editors(通常是 Widget 的子類別)。- Changes made to the structure of the
List
returned byListEditor.getList()
will be reflected in calls made to theEditorSource
. OptionalFieldEditor
可以用來編輯 nullable 或是 resettable 的 bean properties。SimpleEditor
可以用來作為一個 headless property EditorTakesValueEditor
可以將TakesValue<T>
轉換為LeafValueEditor<T>
ValueBoxEditor
可以將ValueBoxBase<T>
轉換為LeafValueEditor<T>
。如果getValueOrThrow()
方法丟出一個ParseException
例外,這個例外會經由一個EditorError
來處理回報。ValueBoxEditorDecorator
是一個簡單的 UI decorator,結合一個ValueBoxBase
與一個Label
,顯示任何ValueBoxBase
所產生的 parse 錯誤。
Driver 的種類
GWT 架構提供了以下幾種頂層的 Driver:
SimpleBeanEditorDriver
- 可以被任何類似 bean 的物件使用。
- 沒有支援更新 subscriptions。
RequestFactoryEditorDriver
- 設計用來與
RequestFactory
整合,以及編輯EntityProxy
的子類別。 - 當遇到
EntityProxy
時,這個 Driver 需要一個RequestContext
用來自動呼叫RequestContext.edit()
。 - 藉由傾聽
RequestFactory
的EventBus
中的EntityProxyChange
事件,可以支援 subscriptions。
參考資料:GWT、On the JVM