UiEditors

类似 Bean 对象的数据绑定

GWT 编辑器框架允许将存储在对象图中的数据映射到编辑器图。典型的场景是将从 RPC 机制返回的对象连接到 UI。

  1. 目标
  2. 快速入门
  3. 定义
  4. 一般工作流程
  5. 编辑器契约
  6. 编辑器委托
  7. 编辑器子类型
    1. LeafValueEditor
    2. HasEditorDelegate
    3. ValueAwareEditor
    4. CompositeEditor
    5. HasEditorErrors
  8. 提供的适配器
  9. 驱动程序类型
  10. 常见问题解答
    1. Editor vs. IsEditor
    2. 只读编辑器
    3. 非常大的对象

目标

  • 减少将数据从对象图移动到 UI 并返回所需的粘合代码量。
  • 与任何类似 Bean 的对象兼容,无论其实现机制(POJO、JSO、RPC、RequestFactory)。
  • 支持编辑器的任意组合。
  • 对于 GWT 2.1 版本之后的版本,建立以下轨迹

快速入门

在您的 gwt.xml 文件中导入 com.google.gwt.editor.Editor 模块。

// Regular POJO, no special types needed
public class Person {
  Address getAddress();
  Person getManager();
  String getName();
  void setManager(Person manager);
  void setName(String name);
}
 
// Sub-editors are retrieved from package-protected fields, usually initialized with UiBinder.
// Many Editors have no interesting logic in them
public class PersonEditor extends Dialog implements Editor<Person> {
  // Many GWT Widgets are already compatible with the Editor framework
  Label nameEditor;
  // Building Editors is usually just composition work
  AddressEditor addressEditor;
  ManagerSelector managerEditor;
 
  public PersonEditor() {
    // Instantiate my widgets, usually through UiBinder
  }
}
 
// A simple demonstration of the overall wiring
public class EditPersonWorkflow{
  // Empty interface declaration, similar to UiBinder
  interface Driver extends SimpleBeanEditorDriver<Person, PersonEditor> {}
 
  // Create the Driver
  Driver driver = GWT.create(Driver.class);
 
  void edit(Person p) {
    // PersonEditor is a DialogBox that extends Editor<Person>
    PersonEditor editor = new PersonEditor();
    // Initialize the driver with the top-level editor
    driver.initialize(editor);
    // Copy the data in the object into the UI
    driver.edit(p);
     // Put the UI on the screen.
    editor.center();
  }
 
  // Called by some UI action
  void save() {
    Person edited = driver.flush();
    if (driver.hasErrors()) {
      // A sub-editor reported errors
    }
    doSomethingWithEditedPerson(edited);
  }
}

定义

  • 类似 Bean 的对象:(以下简称“Bean”)支持通过强类型 Foo getFoo() 方法检索属性,并具有可选的 void setFoo(Foo foo); 方法的对象。
  • 编辑器:支持编辑 Bean 的零个或多个属性的对象。
    • 编辑器可以由任意数量的子编辑器组成,这些子编辑器编辑 Bean 的属性。
    • 大多数编辑器是小部件,但框架不需要这样做。可以创建仅执行程序驱动更改的“无头”编辑器。
  • 驱动程序:用于将 Bean 附加到编辑器的“顶层”控制器。驱动程序负责向下遍历编辑器层次结构以传播数据。示例包括 SimpleBeanEditorDriverRequestFactoryEditorDriver
  • 适配器:提供编辑器框架的“预先设定”行为的一系列类型之一。

一般工作流程

  • 实例化并初始化编辑器。
    • 如果编辑器是基于 UI 的,这通常是调用 UiBinder.createAndBindUi() 的时机。
  • 实例化并初始化驱动程序。
    • 驱动程序通过调用 GWT.create() 创建,初始化的具体细节取决于驱动程序,尽管传递编辑器实例很常见。
    • 因为驱动程序是有状态的,所以驱动程序实例必须与编辑器层次结构实例配对。
  • 通过将 Bean 传递到驱动程序中启动编辑过程。
  • 允许用户与 UI 交互。
  • 调用驱动程序的 flush() 方法将编辑器状态复制到 Bean 层次结构中。
  • 可以选择检查 hasErrors()getErrors() 以确定是否存在客户端输入验证问题。

编辑器契约

基本的 Editor 类型只是一个参数化的标记接口,它表示类型符合编辑器契约或非正式协议。Editor 的唯一预期行为是它将通过以下机制之一提供对其子编辑器的访问

  • 一个至少具有包可见性的实例字段,其名称与将要编辑的属性完全相同,或者为 propertyNameEditor。例如
class MyEditor implements Editor<Foo> {
  // Edits the Foo.getBar() property
  BarEditor bar;
  // Edits the Foo.getBaz() property
  BazEditor bazEditor;
}
  • 一个至少具有包可见性的无参数方法,其名称与将要编辑的属性完全相同,或者为 propertyNameEditor。这允许使用接口来定义编辑器层次结构。例如
interface FooEditor extends Editor<Foo> {
  // Edits the Foo.getBar() property
  BarEditor bar();
  // Edits the Foo.getBaz() property
  BazEditor bazEditor();
}
  • @Path 注解可以用于字段或访问器方法以指定带点的属性路径或绕过隐式命名约定。例如
class PersonEditor implements Editor<Person> {
  // Corresponds to person.getManager().getName()
  @Path("manager.name");
  Label managerName;
}
  • @Ignore 注解可以用于字段或访问器方法,以使编辑器框架忽略其他看起来像是子编辑器的对象。
  • 子编辑器可以为 null。在这种情况下,编辑器框架将忽略这些子编辑器。

在使用类型 Editor<T> 的地方,可以使用类型 IsEditor<Editor<T>> 替代。IsEditor 接口允许组合现有的编辑器行为,而无需在组合的编辑器类型中实现 N 多个委托方法。例如,大多数叶子 GWT 小部件类型都实现了 IsEditor,并且可以直接在基于编辑器的 UI 中使用。通过实现 IsEditor,小部件只需要实现单个 asEditor() 方法,这将小部件与组件编辑器逻辑中可能出现的任何 API 更改隔离开。

编辑器委托

每个 Editor 都有一个同级的 EditorDelegate,它为 Editor 提供框架相关的服务。

  • getPath() 返回编辑器在附加的编辑器层次结构中的当前路径。
  • recordError() 允许编辑器将其输入验证错误报告给其父编辑器,最终报告给驱动程序。可以使用 userData 参数将任意数据附加到生成的 EditorError
  • subscribe() 可用于接收正在编辑的对象的外部更新通知。并非所有驱动程序都支持订阅。在这种情况下,对 subscribe() 的调用可能会返回 null

编辑器子类型

除了 Editor 接口外,编辑器框架还会查找这些特定的接口以提供更复杂编辑器行为的基本构建块。本节将记录这些接口,并提供编辑器框架在运行时如何与 API 交互的示例。所有这些核心编辑器子接口都可以随意混合使用。

LeafValueEditor

LeafValueEditor 用于非对象、不可变或编辑器框架不应向下遍历的任何类型。

  1. setValue() 使用应编辑的值调用(例如 fooEditor.setValue(bean.getFoo());)。
  2. getValue() 在驱动程序将编辑器的状态刷新到 Bean 中时调用。从该方法返回的值将被分配给正在编辑的 Bean(例如 bean.setFoo(fooEditor.getValue());)。

HasEditorDelegate

HasEditorDelegate 为编辑器提供其同级的 EditorDelegate

  1. setEditorDelegate() 在任何值初始化之前调用。

ValueAwareEditor

如果编辑器的行为取决于它正在编辑的值,或者如果编辑器需要显式刷新通知,则可以使用 ValueAwareEditor

  1. setEditorDelegate() 被调用,与 HasEditorDelegate 超级接口一致。
  2. setValue() 使用编辑器负责编辑的值调用。如果该值会影响是否为框架提供子编辑器,则应在此处初始化或取消子编辑器。
  3. 如果调用了 EditorDelegate.subscribe(),则编辑器可能会在任何时间点接收对 onPropertyChange()setValue() 的后续调用。
  4. flush() 由驱动程序以深度优先的方式调用,因此编辑器通常不会刷新其子编辑器。直接修改其同级对象的编辑器应该只在调用 flush() 时这样做,以便允许取消编辑工作流程。

CompositeEditor

CompositeEditor 允许在运行时将未知数量的同类子编辑器添加到编辑器层次结构中。除了为 ValueAwareEditor 描述的行为之外,CompositeEditor 还具有以下附加 API

  1. createEditorForTraversal() 应返回一个规范的子编辑器实例,驱动程序将使用该实例来计算所有已编辑的路径。如果组合编辑器正在编辑一个集合,那么该方法解决了在空集合中没有可用于检查的子编辑器的难题。
  2. setEditorChain()CompositeEditor 提供了对 EditorChain 的访问权限,这允许将组件子编辑器附加和分离到编辑器层次结构中。
  3. getPathElement() 由编辑器框架为每个附加的组件子编辑器调用,以计算 EditorDelegate.getPath() 的返回值。正在编辑可索引数据结构(如 List)的 CompositeEditor 可能会为该方法返回 [index]

HasEditorErrors

HasEditorErrors 表示编辑器希望通过 EditorDelegate.recordError() 接收子编辑器报告的任何未消费的错误。编辑器可以通过调用 EditorError.setConsumed() 来标记 EditorError 为已消费。

提供的适配器

GWT 发行版提供以下编辑器适配器类,它们提供可重用的逻辑。为了减少泛型样板代码,大多数类型都配备了静态的 of() 方法来实例化适配器类型。

  • HasDataEditorList<T> 适配为 HasData<T>
  • HasTextEditorHasText 接口适配为 LeafValueEditor<String>
    • 新的部件应该优先使用 TakesValue<String> 而不是 HasText
  • ListEditor 保持 List<T> 与子编辑器列表同步。
    • ListEditor 使用用户提供的 EditorSource 创建,该 EditorSource 提供子编辑器(通常是部件子类型)。
    • ListEditor.getList() 返回的 List 结构所做的更改将反映在对 EditorSource 的调用中。
    • 示例代码.
  • OptionalFieldEditor 可用于可为空或可重置的 bean 属性。
  • SimpleEditor 可用作无头属性编辑器
  • TakesValueEditorTakesValue<T> 适配为 LeafValueEditor<T>
  • ValueBoxEditorValueBoxBase<T> 适配为 LeafValueEditor<T>。如果 getValueOrThrow() 方法抛出 ParseException,则会通过 EditorError 报告异常。
  • ValueBoxEditorDecorator 是一个简单的 UI 装饰器,它将 ValueBoxBaseLabel 结合起来,以显示包含的 ValueBoxBase 的任何解析错误。

驱动程序类型

GWT 编辑器框架提供以下顶级驱动程序

  • SimpleBeanEditorDriver 可用于任何类似 bean 的对象。
    • 它不支持更新订阅。
  • RequestFactoryEditorDriver 旨在与 RequestFactory 集成并编辑 EntityProxy 子类型。
    • 此驱动程序类型需要 RequestContext 才能在遇到任何 EntityProxy 实例时自动调用 RequestContext.edit()
    • 通过监听 RequestFactoryEventBus 上的 EntityProxyChange 事件来支持订阅。

常见问题解答

编辑器与 IsEditor

问:我的 Widget 应该实现 Editor 接口还是 IsEditor

答:如果 Widget 包含多个子编辑器并且是一个简单的静态层次结构,请使用 Editor 接口。

IsEditor 接口旨在用于视图类型重新使用外部类型提供的编辑器行为时。例如,LabelDecorator 类型将实现 IsEditor,因为它重新使用其 Label 的现有编辑器行为

class LabelDecorator extends Composite implements IsEditor<LeafValueEditor<String>> {
  private final Label wrapped = new Label();
 
  public LabelDecorator() {
    // Construct a pretty UI around the wrapped label
    initWidget(prettyContents);
  }
 
  public LeafValueEditor<String> asEditor() {
    return wrapped.asEditor();
  }
}

类似地,WorkgroupMembershipEditor 可能实现 IsEditor<ListEditor<Person, PersonNameLabel>>

只读编辑器

问:我可以用编辑器来查看只读数据吗?

答:是的,只需不要在驱动程序类型上调用 flush() 方法。RequestFactoryEditorDriver 也提供了一个方便的 display() 方法。

非常大的对象

问:如何编辑具有大量属性的对象?

答:编辑器不必编辑其对等域对象的全部属性。如果你有一个具有许多属性的 BagOfState 类型,则编写几个编辑器类型来编辑概念上相关的属性子集可能是有意义的

class BagOfStateBiographicalEditor implements Editor<BagOfState> {
  AddressEditor address;
  Label name; 
}
 
class BagOfStateUserPreferencesEditor implements Editor<BagOfState> {
  CheckBox likesCats;
  CheckBox likesDogs;
}

这些编辑器是否同时显示或顺序显示是一个用户体验问题。编辑器框架允许多个编辑器编辑同一个对象


class HasBagOfStateEditor implements Editor<HasBagOfState> { @Editor.Path("state") BagOfStateBiographicalEditor bio; @Editor.Path("state") BagOfStateUserPreferencesEditor prefs; }