UiEditors
类似 Bean 对象的数据绑定
GWT 编辑器框架允许将存储在对象图中的数据映射到编辑器图。典型的场景是将从 RPC 机制返回的对象连接到 UI。
目标
- 减少将数据从对象图移动到 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 附加到编辑器的“顶层”控制器。驱动程序负责向下遍历编辑器层次结构以传播数据。示例包括
SimpleBeanEditorDriver
和RequestFactoryEditorDriver
。 - 适配器:提供编辑器框架的“预先设定”行为的一系列类型之一。
一般工作流程
- 实例化并初始化编辑器。
- 如果编辑器是基于 UI 的,这通常是调用
UiBinder.createAndBindUi()
的时机。
- 如果编辑器是基于 UI 的,这通常是调用
- 实例化并初始化驱动程序。
- 驱动程序通过调用
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
用于非对象、不可变或编辑器框架不应向下遍历的任何类型。
setValue()
使用应编辑的值调用(例如fooEditor.setValue(bean.getFoo());
)。getValue()
在驱动程序将编辑器的状态刷新到 Bean 中时调用。从该方法返回的值将被分配给正在编辑的 Bean(例如bean.setFoo(fooEditor.getValue());
)。
HasEditorDelegate
HasEditorDelegate
为编辑器提供其同级的 EditorDelegate
。
setEditorDelegate()
在任何值初始化之前调用。
ValueAwareEditor
如果编辑器的行为取决于它正在编辑的值,或者如果编辑器需要显式刷新通知,则可以使用 ValueAwareEditor
。
setEditorDelegate()
被调用,与HasEditorDelegate
超级接口一致。setValue()
使用编辑器负责编辑的值调用。如果该值会影响是否为框架提供子编辑器,则应在此处初始化或取消子编辑器。- 如果调用了
EditorDelegate.subscribe()
,则编辑器可能会在任何时间点接收对onPropertyChange()
或setValue()
的后续调用。 flush()
由驱动程序以深度优先的方式调用,因此编辑器通常不会刷新其子编辑器。直接修改其同级对象的编辑器应该只在调用flush()
时这样做,以便允许取消编辑工作流程。
CompositeEditor
CompositeEditor
允许在运行时将未知数量的同类子编辑器添加到编辑器层次结构中。除了为 ValueAwareEditor
描述的行为之外,CompositeEditor
还具有以下附加 API
createEditorForTraversal()
应返回一个规范的子编辑器实例,驱动程序将使用该实例来计算所有已编辑的路径。如果组合编辑器正在编辑一个集合,那么该方法解决了在空集合中没有可用于检查的子编辑器的难题。setEditorChain()
为CompositeEditor
提供了对EditorChain
的访问权限,这允许将组件子编辑器附加和分离到编辑器层次结构中。getPathElement()
由编辑器框架为每个附加的组件子编辑器调用,以计算EditorDelegate.getPath()
的返回值。正在编辑可索引数据结构(如List
)的CompositeEditor
可能会为该方法返回[index]
。
HasEditorErrors
HasEditorErrors
表示编辑器希望通过 EditorDelegate.recordError()
接收子编辑器报告的任何未消费的错误。编辑器可以通过调用 EditorError.setConsumed()
来标记 EditorError
为已消费。
提供的适配器
GWT 发行版提供以下编辑器适配器类,它们提供可重用的逻辑。为了减少泛型样板代码,大多数类型都配备了静态的 of()
方法来实例化适配器类型。
HasDataEditor
将List<T>
适配为HasData<T>
。HasTextEditor
将HasText
接口适配为LeafValueEditor<String>
。- 新的部件应该优先使用
TakesValue<String>
而不是HasText
。
- 新的部件应该优先使用
ListEditor
保持List<T>
与子编辑器列表同步。ListEditor
使用用户提供的EditorSource
创建,该EditorSource
提供子编辑器(通常是部件子类型)。- 对
ListEditor.getList()
返回的List
结构所做的更改将反映在对EditorSource
的调用中。 - 示例代码.
OptionalFieldEditor
可用于可为空或可重置的 bean 属性。SimpleEditor
可用作无头属性编辑器TakesValueEditor
将TakesValue<T>
适配为LeafValueEditor<T>
ValueBoxEditor
将ValueBoxBase<T>
适配为LeafValueEditor<T>
。如果getValueOrThrow()
方法抛出ParseException
,则会通过EditorError
报告异常。ValueBoxEditorDecorator
是一个简单的 UI 装饰器,它将ValueBoxBase
与Label
结合起来,以显示包含的ValueBoxBase
的任何解析错误。
驱动程序类型
GWT 编辑器框架提供以下顶级驱动程序
SimpleBeanEditorDriver
可用于任何类似 bean 的对象。- 它不支持更新订阅。
RequestFactoryEditorDriver
旨在与RequestFactory
集成并编辑EntityProxy
子类型。- 此驱动程序类型需要
RequestContext
才能在遇到任何EntityProxy
实例时自动调用RequestContext.edit()
。 - 通过监听
RequestFactory
的EventBus
上的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;
}