這篇是將 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
Listreturned 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