Java™ 图形用户界面 (GUI) 应用程序的大量开发时间都用于将域对象的数据简单地移入 GUI 组件,然后再从 GUI 组件返回给域对象。近年来,几种数据绑定框架走在了自动同步数据过程研究的最前沿。本文将说明什么是数据绑定框架,介绍几种流行的 Java GUI 数据绑定框架,并将分析使用数据绑定的优缺点。
很多流行的 Web 应用程序都有视图层的特性,视图层足够智能可以将请求和应答变量与 HTML 输入标记同步。此过程可以轻松地完成,因为用户输入是通过 Web 应用程序的结构层和 HTTP 来路由的。而另一方面,Java GUI 应用程序经常都不能支持这种特性。无论是用 Standard Widget Toolkit (SWT) 编写还是用 Swing 编写,这些 Java GUI 应用程序的域对象与其 GUI 控件 (通常也称为 组件) 之间通常都没有定义好的路径。
打乱顺序最糟糕的结果是造成数据混乱,最幸运的结果是大块样本同步代码出现 bug。参考 清单 1 中的代码引用。这是一个称为 FormBean
的简单的域对象的定义。当一个对话框需要使用此数据时,对话框必须从域对象中提取此数据并将数据插入组件才能显示,如位于构建程序的末尾的方法调用中所示。相应地,在用户更改信息后,必须将此数据从 GUI 组件中提取出来并放回域模型中。这种往复过程是通过 syncBeanToComponents()
和 syncComponentsToBean()
方法来执行的。最后,请注意对 GUI 组件的引用在对象范围内必须保持可用,这样才能在同步方法中访问这些组件。
清单 1. 没有数据绑定的 Swing 对话框
package com.nfjs.examples; import com.jgoodies.forms.layout.FormLayout; import com.jgoodies.forms.layout.CellConstraints; import com.jgoodies.forms.builder.DefaultFormBuilder; import javax.swing.*; import java.awt.event.ActionEvent; public class NoBindingExample { private JFrame frame; private JTextField firstField; private JTextField lastField; private JTextArea descriptionArea; private FormBean bean; public NoBindingExample() { frame = new JFrame(); firstField = new JTextField(); lastField = new JTextField(); descriptionArea = new JTextArea(6, 6); DefaultFormBuilder builder = new DefaultFormBuilder(new FormLayout("r:p, 2dlu, f:p:g")); builder.setDefaultDialogBorder(); builder.append("First:", firstField); builder.append("Last:", lastField); builder.appendRelatedComponentsGapRow(); builder.appendRow("p"); builder.add(new JLabel("Description:"), new CellConstraints(1, 5, CellConstraints.RIGHT, CellConstraints.TOP), new JScrollPane(descriptionArea), new CellConstraints(3, 5, CellConstraints.FILL, CellConstraints.FILL)); builder.nextRow(2); builder.append(new JButton(new MessageAction())); frame.add(builder.getPanel()); frame.setSize(300, 300); bean = new FormBean(); syncBeanToComponents(); } private void syncBeanToComponents() { firstField.setText(bean.getFirst()); lastField.setText(bean.getLast()); descriptionArea.setText(bean.getDescription()); } private void syncComponentsToBean() { bean.setFirst(firstField.getText()); bean.setLast(lastField.getText()); bean.setDescription(descriptionArea.getText()); } public JFrame getFrame() { return frame; } private class FormBean { private String first; private String last; private String description; public FormBean() { this.first = "Scott"; this.last = "Delap"; this.description = "Description"; } public String getFirst() { return first; } public void setFirst(String first) { this.first = first; } public String getLast() { return last; } public void setLast(String last) { this.last = last; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } } private class MessageAction extends AbstractAction { public MessageAction() { super("Message"); } public void actionPerformed(ActionEvent e) { syncComponentsToBean(); JOptionPane.showMessageDialog(null, "First name is " + bean.getFirst()); } } public static void main(String[] args) { NoBindingExample example = new NoBindingExample(); example.getFrame().show(); } } |
救援的数据绑定
|
此示例只是一个简单示例,如果要计算构建程序中的组件引用指定,还需要有另外 10 行代码。如果向 Bean 中添加新字段,则需要添加另外三行代码进行初始化以及在 GUI 组件和域模型实现双向同步。重复编写这段代码是十分令人厌烦的,经常会导致将错误引入应用程序中。幸运的是,有更好的解决方案可用。
数据绑定框架使开发人员可以轻松地将 JavaBean 属性与 GUI 组件 “粘” 在一起。JavaBean 属性通常被一个字符串引用,该字符串用于告诉数据绑定框架在 JavaBean 上查找相应的 getter 和 setter。例如,"first"
表示在给定 JavaBean 上有 getFirst()
和 setFirst()
方法。组件将被数据自动初始化。当组件中的值发生改变时,关联的 JavaBean 属性也会随之改变。同样地,JavaBean 支持属性更改侦听程序,因此当 GUI 组件的相应 JavaBean 属性发生改变时,也可以更新 GUI 组件。
还可以配置流行的 Java 数据绑定框架何时同步更改 (通常在按下按键时、单击鼠标时或光标丢失时)。这些数据绑定框架还支持各种 GUI 组件,例如文本字段、复选框、列表和表。
清单 2 显示了重新编写 清单 1 中的代码引用以使用 JGoodies 数据绑定框架。
清单 2. 使用 JGoodies 数据绑定的同一个 Swing 对话框
package com.nfjs.examples; import com.jgoodies.forms.layout.FormLayout; import com.jgoodies.forms.layout.CellConstraints; import com.jgoodies.forms.builder.DefaultFormBuilder; import com.jgoodies.binding.beans.BeanAdapter; import com.jgoodies.binding.adapter.BasicComponentFactory; import javax.swing.*; import java.awt.event.ActionEvent; public class BindingExample { private JFrame frame; private FormBean bean; public BindingExample() { frame = new JFrame(); bean = new FormBean(); BeanAdapter adapter = new BeanAdapter(bean); JTextField firstField = BasicComponentFactory.createTextField( adapter.getValueModel("first")); JTextField lastField = BasicComponentFactory.createTextField( adapter.getValueModel("last")); JTextArea descriptionArea = BasicComponentFactory.createTextArea( adapter.getValueModel("description")); DefaultFormBuilder builder = new DefaultFormBuilder(new FormLayout("r:p, 2dlu, f:p:g")); builder.append("First:", firstField); builder.append("Last:", lastField); builder.appendRelatedComponentsGapRow(); builder.appendRow("p"); builder.add(new JLabel("Description:"), new CellConstraints(1, 5, CellConstraints.RIGHT, CellConstraints.TOP), new JScrollPane(descriptionArea), new CellConstraints(3, 5, CellConstraints.FILL, CellConstraints.FILL)); builder.nextRow(2); builder.append(new JButton(new MessageAction())); frame.add(builder.getPanel()); frame.setSize(300, 300); } public JFrame getFrame() { return frame; } public class FormBean { //Same as above } private class MessageAction extends AbstractAction { public MessageAction() { super("Message"); } public void actionPerformed(ActionEvent e) { JOptionPane.showMessageDialog(null, "First name is " + bean.getFirst()); } } public static void main(String[] args) { BindingExample example = new BindingExample(); example.getFrame().show(); } } |
我会在后面介绍详细的实现方法。现在,请注意那些在与第一个示例对比时所没有的内容:
- 没必要仅因为要同步而公开对组件的引用。这些引用不会被传播到构建程序范围之外。
- 未提供任何一种同步方法。
- 构建程序中没有初始同步过程用于填充组件。
- 显示对话框之前没有同步操作。
不管所有这些条目现在都已丢失的事实,此示例将完全执行与第一个示例相同的操作。
|
介绍整个 JGoodies 数据绑定框架不在本文讨论范围内。但是,看一看 清单 2 所示的示例的实现细节十分有用。下面的两行揭示了所有奥秘:
BeanAdapter adapter = new BeanAdapter(bean);
JTextField firstField = BasicComponentFactory.createTextField(adapter.getValueModel("first"));
第一行用于创建一个 JGoodies 对象,名为 BeanAdapter
,该对象用于创建值模型对象。值模型用于定义一种一般方法来访问 JavaBean 属性,而无需知道该属性名称的详细信息。清单 3 显示了 ValueModel
接口定义。
清单 3. ValueModel 接口
public interface ValueModel { java.lang.Object getValue(); void setValue(java.lang.Object object); void addValueChangeListener(PropertyChangeListener propertyChangeListener); void removeValueChangeListener(PropertyChangeListener propertyChangeListener); } |
BasicComponentFactory
类含有创建 Swing 组件的方法,这些组件将与提供的 ValueModel
绑定在一起。第二行将使用 BasicComponentFactory
来创建一个 JTextField
。在这种情况下,JTextField
将与 FormBean
的 "first"
属性绑定在一起。JGoodies 数据绑定 API 将执行用来源于 FormBean
的数据对文本字段进行初始化操作的其余过程,它还将在文本字段中所作的所有更改都同步回 FormBean
中。
|
整个同步执行过程仍可能好像是在变魔术一样虚幻。但是,事实并不如此。几乎所有流行的 GUI 组件的背后都有一个模型。数据绑定框架的任务是获取存储在域对象中的值再导入模型中。框架采用两种方法来执行这项任务。
一种方法是调整被绑定的 Bean 字段变为组件模型本身。使用这种方法,当组件的视图部分尝试检索或修改值时,它可直接转到 Bean 中的值。JGoodies 数据绑定框架在很多情况下都使用了这种方法。
图 1 显示了 JGoodies 怎样使用 DocumentAdapter
和 PropertyAdapter
类来装饰 Bean 以将其用于 JTextComponent
的模型。
图 1. JGoodies 调整字段用于 JTextComponent 模型
将模型与 GUI 值同步的另一种方法是当一个值在另一端发生改变时自动调用关系两端的 getter 和 setter。JFace 数据绑定框架使用了这种技术以与 SWT 结合使用。
清单 4 显示了与前述相同的示例用 SWT 和 JFace 数据绑定重写后的结果。这个框架充当的是将字段联系在一起的上下文对象。请注意,这里使用了三个 context.bind()
方法调用,用于将文本控件与 FormBean
字段关联起来。
清单 4. 使用 JFace 数据绑定的同一个 Swing 对话框
import org.eclipse.jface.examples.databinding.nestedselection.BindingFactory; import org.eclipse.jface.internal.databinding.provisional.DataBindingContext; import org.eclipse.jface.internal.databinding.provisional.description.Property; import org.eclipse.swt.SWT; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.MessageBox; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Text; public class JFaceBindingExample { private Shell shell; private FormBean bean; public void run() { Display display = new Display(); shell = new Shell(display); GridLayout gridLayout = new GridLayout(); gridLayout.numColumns = 2; shell.setLayout(gridLayout); bean = new FormBean(); DataBindingContext context = BindingFactory.createContext(shell); Label label = new Label(shell, SWT.SHELL_TRIM); label.setText("First:"); GridData gridData = new GridData(GridData.FILL_HORIZONTAL); Text text = new Text(shell, SWT.BORDER); text.setLayoutData(gridData); context.bind(text, new Property(bean, "first"), null); label = new Label(shell, SWT.NONE); label.setText("Last:"); text = new Text(shell, SWT.BORDER); context.bind(text, new Property(bean, "last"), null); gridData = new GridData(); gridData.horizontalAlignment = SWT.CENTER; gridData.horizontalSpan = 2; Button button = new Button(shell, SWT.PUSH); button.setLayoutData(gridData); button.setText("Message"); button.addSelectionListener(new SelectionAdapter() { public void widgetSelected(SelectionEvent e) { MessageBox messageBox = new MessageBox(shell); messageBox.setMessage("First name is " + bean.getFirst()); messageBox.open(); } }); shell.pack(); shell.open(); while (!shell.isDisposed()) { if (!display.readAndDispatch()) display.sleep(); } display.dispose(); } public static void main(String[] args) { JFaceBindingExample example = new JFaceBindingExample(); example.run(); } } |
|
现在有大量优秀的数据绑定框架可以与 Java GUI API 结合使用。下面是为与 Swing 应用程序结合使用创造出来的主要开源 API。
流行的 JGoodies 数据绑定 API 几年前就以 Java.net 上的开源项目的形式出现了。它是由 Karsten Lentzsch 编写的,他还是流行的 JGoodies FormLayout 的作者。此框架存在的历史最久,已经经过多次修改和 bug 修正以使其用于产品用途时更加稳定。
Spring Rich Client Platform (RCP) 项目 —— 流行的 Spring Application Framework 的子项目 —— 还包含一个 Swing 数据绑定框架。Spring RCP 和 JGoodies 都受到了 VisualWorks Smalltalk 中的数据绑定设计的影响。Spring RCP 还没有完成 V1.0 发布。这说明,代码库的数据绑定部分十分稳定并且被很多开发人员使用着。
Java.net 中的 SwingLabs 项目已经开展了多年的 Swing 数据绑定框架开发工作。不过,在此项目中取得的成果最近被转到了一个由 Sun Microsystems 发起的新数据绑定 Java Specification Request (JSR) 上。
这是最近创建的 JSR 项目,由 Sun 的 Scott Violet 及专家组成员 Ben Galbraith 和 Karsten Lentzsch 共同领导,这个项目旨在为在桌面环境和服务器环境中使用的数据绑定提供标准的 API。JSR 295 尚处于开发阶段,因此它不适于现在就需要这类解决方案的开发人员。
|
有两个主要的开源数据绑定 API 用于与 SWT 结合使用。
Jaysoft 接入了流行的 JGoodies 数据绑定 API 用于与 SWT 结合使用。核心类几乎同 JGoodies 一样。特定于 Swing 的模型则被适用于 SWT 控件的模型所替代。
最近出现的另一个 Java 数据绑定新成员是 JFace 数据绑定框架。Eclipse V3.2 发布版中附带了该 API 的临时版本。不同于 SWTBinding/JGoodies 框架,JFace 数据绑定是从头开始构建的,专门与 SWT 和 JFace 结合使用。
|
除了解决同步问题之外,在应用程序中使用数据绑定框架还有其他优点。由于是重复使用同一段同步代码,而不是创建自己的同步代码,因此出现的错误会少一些。另一个主要的优点是获得应用程序可测试性。
流行的 Presentation Model (请参阅 参考资料) 提倡将应用程序的状态与业务逻辑分开放入模型层中,而模型层是从视图的 GUI 控件中分离出来的。模型的状态频繁与视图同步,如图 2 所示。
图 2. 使用 Presentation Model 的关系
这类设计允许测试应用程序的所有业务逻辑而无需将视图实例化。例如,当总数大于 100 时启用表中的某些控件,有一个 "if total > 100"
的启用条件,还有一个基于此条件评估的相关状态。
使用 Presentation Model 模式,此状态被设在 Presentation Model 的变量中,并与视图同步以修改控件的启用。正因为这样,才能够测试逻辑而无需访问视图中的 GUI 组件。
用 SWT 和 Swing 通常很难访问 GUI 组件并模拟(mock)这些组件。针对 Presentation Model 运行所有测试,因为 Presentation Model 包含有条件的逻辑和一个储存随执行而更改的状态的空间。整个模式的一个难点是何时或怎样在 Presentation Model 和视图之间来回同步数据。在数据绑定前,解决这个问题很难。现在,这个问题就像在 Presentation Model 中将控件绑定到字段上或关联的域对象上一样容易。
|
在应用程序中使用数据绑定框架有一些缺点。首先,应用程序更难调试,因为附加的绑定层使追踪控件与域对象之间的数据流变得更难。不过,当熟悉了所使用框架的实现规范后,调试过程会变得更容易。
由于使用字符串表示属性,因此应用程序在重构期间很可能变得更脆弱。考虑一下清单 5 中的代码引用。字符串 "first"
用于通知 JFace 数据绑定框架绑定到 getFirst() / setFirst()
属性上。将 getFirst()
和 setFirst()
重构为 getFirstName()
和 setFirstName()
需要将字符串更改为 "firstName"
。目前的 IDE 重构工具不会捕捉这种变化。
清单 5. 区域重构不捕捉
context.bind(text, new Property(bean, "first"), null); . . . private class FormBean { private String first; ... public FormBean() { this.first = "Scott"; this.last = "Delap"; this.description = "Description"; } public String getFirst() { return first; } public void setFirst(String first) { this.first = first; } . . . } |
|
无论是否在 SWT 或 Swing 中进行开发,在项目中使用数据绑定框架好处很多。没有人喜欢编写或维护样本 GUI 至域模型同步代码。我希望这篇入门级文章已经向您展示了 Java 数据绑定框架是如何能够让您从这些工作中解脱出来的。附带的好处是这些数据绑定框架在与适当的 GUI 设计模式结合使用时能够提高可测试性。
学习
- 您可以参阅本文在 developerWorks 全球站点上的 英文原文 。
- 阅读 ClientJava.com 上的 "How Many Data Binding Frameworks = A Bad Thing" 文章。
- 访问 Eclipse Foundation 维基,了解关于 JFace 数据绑定 的更多信息。
- 查看新的 JSR 295: Beans Binding。
- 查看 Martin Fowler 对 Presentation Model 的理解。
- 访问 IBM developerWorks 的 Eclipse 项目资源,扩展使用 Eclipse 的技巧。
- 访问 developerWorks Open source 专区 以获得大量的 how-to 信息、工具和项目更新信息,可以帮助您利用开放源码技术进行开发,并与 IBM 的产品结合使用。
- 随时关注developerWorks 技术事件和网络广播。
获得产品和技术
- 查阅 JGoodies Binding 项目。
- 查阅 Spring Rich Client Project (RCP)。
- 下载 SWTBinding。
- 在 IBM alphaWorks 查阅最新的 Eclipse 技术下载。
- 使用 IBM 试用软件 改进您的下一个开放源码开发项目,这些软件可以通过下载或从 DVD 中获得。
讨论
- 通过参与 developerWorks blogs 加入 developerWorks 社区。
Scott Delap 是专注于 Java EE 和富 Java 客户机的独立顾问。他在 JavaOne 发表过论文并积极活跃于桌面 Java 社区。他还是 ClientJava.com 的管理员,该网站是关于桌面 Java 开发的门户网站。ClientJava.com 频繁出现于 Web 上,从 JavaBlogs 一直到 Sun Microsystems 的网站。 |