這篇是將 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 架構大致包含幾個步驟:

  1. 實作與初始化 Editor。
  2. 實作與初始化 Driver。
  3. 將 bean 傳入 Driver 開始進行資料的編輯。
  4. 讓使用者與 UI 互動。
  5. 呼叫 Driver 的 flush() 函數將 Editor 中的資料複製回 bean。
  6. 透過 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;
}

其中 BarEditorBazEditor 都是子 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>
  • HasTextEditorHasText 介面轉為 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 by ListEditor.getList() will be reflected in calls made to the EditorSource.
  • OptionalFieldEditor 可以用來編輯 nullable 或是 resettable 的 bean properties。
  • SimpleEditor 可以用來作為一個 headless property Editor
  • TakesValueEditor 可以將 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()
    • 藉由傾聽 RequestFactoryEventBus 中的 EntityProxyChange 事件,可以支援 subscriptions。

參考資料:GWTOn the JVM