了解Eclipse中的JFace数据绑定,第2部分:绑定的基础知识

开始之前

关于本系列

使用数据绑定 API 可以将您从必须编写样本同步代码的痛苦中解脱出来。JFace 数据绑定 API 为用户界面 (UI) 提供了这种功能,该功能是用 Standard Widget Toolkit (SWT) 和 JFace 编写的。

“了解 Eclipse 中的 JFace 数据绑定” 系列教程的 第 1 部分 说明了数据绑定框架的用途,介绍了几个流行的 Java GUI 数据绑定框架,并给出了使用数据绑定的优点和缺点。作为该系列的第 2 部分,本教程将介绍基本的 API 组件。第 3 部分将转向介绍高级主题,例如表、转换程序及验证。

关于本教程

本教程说明了使用数据绑定 API 的原因,然后将向您介绍如何使用 JFace 数据绑定 API 的核心组件,而把说明底层如何工作的内容放到了第 3 部分进行介绍。

先决条件

本教程面向拥有一定 Java™ 编程语言和 Eclipse 使用经验的开发人员,而且必须对 SWT 和 JFace 有一定的基本了解。

系统要求

要运行示例,则必须要有一个 Eclipse 软件开发包 (SDK) 及一台能够运行该软件开发包的计算机。

在域对象和控件之间同步数据

同步需求

桌面应用程序往往都有长期使用的对象,这些对象大都包含用户可视的数据。例如,在人员对象的名字字段中所做的更改通常需要被反映到用户编辑该对象时所在的表单中。这意味着要更新用于显示数据的文本字段小部件。如果更改是在文本字段小部件中发起的,则需要更新人员对象。如果人员对象由于业务原因而发生了更改,则显示更改的小部件也需要改变。

很多小部件,例如表和列表,都有可以简化此过程的模型。更改此模型将自动通知小部件。大多数应用程序数据并不以特定于 SWT 的模型为其形式。例如在使用表的情况下,用于填充表的数据经常是从服务器或数据库中查询到的值的 java.util.List 形式。进一步来考虑更复杂的情况,事实上一些小部件(如文本字段)根本就没有模型;它们只有包含显示数据的小部件所固有的简单属性。

样本同步

两个主要的 Java 小部件工具包 Swing 和 SWT 的小部件都不识别数据。这意味着将由您来决定如何管理同步进程。我们来看下面的示例以帮助您理解其含义。请按照以下步骤执行操作:

打开 Eclipse V3.2 并创建一个新的工作区。

在菜单中选择 File > Import。系统将打开 Eclipse 项目导入向导(参见图 1)。

图 1. Eclipse 项目导入向导

选中 Existing Projects into Workspace,然后单击 Next。

在下一个屏幕中,选中 Select archive file,然后导入可在本教程的 下载 部分下载到的 project.zip 文件(参见图 2)。现在,工作区中应当包含了一个类似图 3 所示的项目。

图 2. 选择项目归档文件

图 3. 项目导入后的工作区

单击 Eclipse 运行按钮旁边的箭头,然后选择 NoBinding 运行目标。系统将显示一个类似图 4 所示的窗口。

图 4. 运行示例

此时,用应用程序执行一些练习十分有帮助:

请注意,任何一个文本框中都没有显示文本。单击 Change Name 以将文本更改为 James Gosling。

将 First 和 Last 名称字段更改为选定的任意文本。

单击 Update Text From Person Bean。文本将恢复为 James Gosling。产生这个结果的原因是所做的字段更改并未与 Person Bean 进行同步。

重新更改文本,然后单击 Update Person Bean From Text。

重新更改文本,然后单击 Update Text from Person Bean。文本将更改回第一次输入的文本,因为在单击按钮时这些值已与 Person Bean 手动同步。

此示例的代码如下所示。

清单 1. 具有手动同步功能的示例应用程序

public class Person {  private String first;  private String last;  public Person(String first, String last) {  this.first = first;  this.last = last;  }  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 class NoBindingExample {  private Person person;  private Text firstText;  private Text lastText;  private void createControls(Shell shell) {  GridLayout gridLayout = new GridLayout();  gridLayout.numColumns = 2;  shell.setLayout(gridLayout);  Label label = new Label(shell, SWT.SHELL_TRIM);  label.setText("First:");  GridData gridData = new GridData(GridData.FILL_HORIZONTAL);  this.firstText = new Text(shell, SWT.BORDER);  this.firstText.setLayoutData(gridData);  label = new Label(shell, SWT.NONE);  label.setText("Last:");  this.lastText = new Text(shell, SWT.BORDER);  gridData = new GridData(GridData.FILL_HORIZONTAL);  this.lastText.setLayoutData(gridData);  }  private void createButtons(Shell shell) {  GridData gridData;  gridData = new GridData();  gridData.horizontalAlignment = SWT.CENTER;  gridData.horizontalSpan = 2;  Button button = new Button(shell, SWT.PUSH);  button.setLayoutData(gridData);  button.setText("Change Name");  button.addSelectionListener(new SelectionAdapter() {   public void widgetSelected(SelectionEvent e) {   updatePerson();   synchronizePersonToUI();   }  });  gridData = new GridData();  gridData.horizontalAlignment = SWT.CENTER;  gridData.horizontalSpan = 2;  button = new Button(shell, SWT.PUSH);  button.setLayoutData(gridData);  button.setText("Update Person Bean From Text");  button.addSelectionListener(new SelectionAdapter() {   public void widgetSelected(SelectionEvent e) {   synchronizeUIToPerson();   }  });  gridData = new GridData();  gridData.horizontalAlignment = SWT.CENTER;  gridData.horizontalSpan = 2;  button = new Button(shell, SWT.PUSH);  button.setLayoutData(gridData);  button.setText("Update Text From Person Bean");  button.addSelectionListener(new SelectionAdapter() {   public void widgetSelected(SelectionEvent e) {   synchronizePersonToUI();   }  });  }  private void updatePerson() {  person.setFirst("James");  person.setLast("Gosling");  }  private void synchronizePersonToUI() {  this.firstText.setText(this.person.getFirst());  this.lastText.setText(this.person.getLast());  }  private void synchronizeUIToPerson() {  this.person.setFirst(this.firstText.getText());  this.person.setLast(this.lastText.getText());  }  public static void main(String[] args) {  NoBindingExample example = new NoBindingExample();  example.run();  }  public void run() {  this.person = new Person("Larry", "Wall");  Display display = new Display();  Shell shell = new Shell(display);  createControls(shell);  createButtons(shell);  shell.pack();  shell.open();  while (!shell.isDisposed()) {   if (!display.readAndDispatch())   display.sleep();  }  display.dispose();  }}

查看代码

清单 1 的开头定义了一个遵循 JavaBean 规范的简单的 Person 类。特别地,它为每个属性配备了 getter 和 setter 方法。清单接下来定义了 NoBindingExample 类。主要方法实例化了一个类的实例,并立即委托给 run() 方法。run() 方法负责创建 UI 并将启动显示示例所需的相应的 SWT 结构。

run() 方法首先将创建一个 Shell,然后将调用构建 UI 部件的 createControls() 方法。接下来,它将调用 createButtons() 方法,该方法用于创建三个按钮。每个按钮都配有鼠标侦听程序,该侦听程序将调用针对示例实例的特定方法。

这种设计会带来的问题

数以千计的应用程序都是用类似上述设计的代码编写的。但是,这样做会带来很多问题:

Person Bean 最初包含值 Larry Wall。应用程序一开始不会显示该值,因为 Person Bean 在启动时并未与文本字段同步。

必须保持对两个文本字段的引用可以为两个同步方法所用。

必须编写样本同步代码。

确定何时在 Person bean 和文本字段之间同步值是一个人工过程。

即使本例的应用程序不需要配有在 Person bean 和文本字段之间来回同步值的按钮,我们仍然必须分析、编码和维护何时调用同步方法的进程。如果文本字段可以反映 Person bean,并且用 API 来保证数据同步(让您可以更轻松地将精力集中在更紧迫的要求上),情况可能会较为简单些。

数据绑定的奥秘

幸运的是,上一部分中所需的 API 并不是一个梦想。有很多框架可用于与 Java 语言结合使用来解决这个问题。它们通常都被归类到术语 数据绑定 下。数据绑定框架的用途就如其名称隐含的内容一样:它们在两个点之间绑定数据;当一端的数据发生更改时,绑定关系的另一端的数据也会被更新。这就是前面的示例所需要的那类功能。

Eclipse V3.2 将一个临时版本的数据绑定 API 附在了 org.eclipse.jface.databinding 插件中,可以使用该插件开发 SWT 和 JFace 应用程序。未来版本的 Eclipse 可能由于功能增强和重新设计而包含不同版本的 API。这并不会限制当前 API 的有效性,当前 API 稳定而且包括很多功能。本教程的其余部分将使用该 API 来重新设计先前的示例。

导入数据绑定

虽然可以使用二进制版本的 JFace 数据绑定(项目归档文件中的示例现已在 IDE 中运行),但是,在开发过程中将源数据绑定作为一个引用来导入十分有用。可以使用 Eclipse 导入向导来执行此操作,如下所示:

从菜单中选择 File > Import。

选择 Plug-ins and Fragments,如图 5 所示,然后单击 Next。

图 5. 导入已有插件

在下一个屏幕中,将底部的 Import As 选项更改为 Projects with Source Folders,然后再次单击 Next,如图 6 所示。

Figure 6. 将 Import As 选项更改为 Projects with Source Folders

从列表中选择 org.eclipse.jface.databinding 项目并将其移至右侧,如图 7 所示。单击 Finish 以导入此项目。

图 7. 选择数据绑定插件

展开新导入的项目。图 8 显示了得到的软件包列表。

图 8. 导入后的工作区

使用数据绑定

我们先不详细介绍 JFace 数据绑定,而是先来使用一下,然后再了解数据绑定是怎样在底层工作的。请按照以下步骤执行操作:

在数据绑定教程项目中任意创建一个新软件包,方法为在 src 文件夹上单击鼠标右键,然后从弹出式菜单中选择 New > Package。

将 NoBindingExample 类从 com.developerworks.nobinding 软件包复制到新创建的软件包中。

在该类上单击鼠标右键,然后选择 Refactor > Rename,将类重命名为 BindingExample。

将清单 2 中的代码粘贴到该类中的 main() 方法定义前。

清单 2. createContext() 方法

public static DataBindingContext createContext() {  DataBindingContext context =  new DataBindingContext();  context.addObservableFactory(  new NestedObservableFactory(context));  context.addObservableFactory(  new BeanObservableFactory(   context,   null,   new Class[] { Widget.class }));  context.addObservableFactory(  new SWTObservableFactory());  context.addObservableFactory(  new ViewersObservableFactory());  context.addBindSupportFactory(  new DefaultBindSupportFactory());  context.addBindingFactory(  new DefaultBindingFactory());  context.addBindingFactory(  new ViewersBindingFactory());  return context;}

根据需要修改已导入的任何内容,然后删除 synchronizeUIToPerson() 方法。

从 createButtons() 方法中删除用于创建 Update Person Bean From Text 按钮的那段代码。

将清单 3 中的代码粘贴到 createControls() 方法的末尾。

清单 3. 将文本小部件绑定到 Person Bean

 DataBindingContext ctx = createContext();  ctx.bind(firstText,  new Property(this.person, "first"),  null);  ctx.bind(lastText,  new Property(this.person, "last"),  null);

在新修改的类上单击鼠标右键,然后从弹出式菜单中选择 Run As > SWT Application。应当会看到一个类似图 9 的窗口。

图 9. 修改后的示例

请注意,文本小部件中包含初始值 Larry 和 Wall。这一点不同于先前的示例,因为先前的示例不会同步初始 Bean 值,而这里的数据绑定已经自动处理了这个问题。在 First 字段中键入一些字符,然后单击 Update Text From Person Bean。文本将恢复为其初始值。

在 First 字段中再次键入一些字符,而且切换到 Last 字段。再次单击 Update Text From Person Bean。更改的文本这一次不会恢复为初始值。数据绑定在焦点消失后将文本小部件中的值自动同步到了 Person Bean 的第一个 String 变量中。

如何变魔术:Observable

现在您已经看到了 JFace 数据绑定如何在实际的应用程序中同步数据。您可能还有一个疑问:“这是如何做到的?”

任何数据绑定框架要执行的第一步操作都是提取出获取值、设定值及侦听更改的概念到通用的实现内。当引用在大部分框架的代码中的概念时,可以使用此通用实现。然后可以针对各种情况编写实现来处理特定细节。

JFace 数据绑定将在 IObservable 和 IObservableValue 接口中提取这些概念,如下所示。

清单 4. IObservable 和 IObservableValue 接口

public interface IObservable {  public void addChangeListener(IChangeListener listener);  public void removeChangeListener(IChangeListener listener);  public void addStaleListener(IStaleListener listener);  public void removeStaleListener(IStaleListener listener);  public boolean isStale();  public void dispose();}public interface IObservableValue extends IObservable {  public Object getValueType();  public Object getValue();  public void setValue(Object value);  public void addValueChangeListener(IValueChangeListener listener);  public void removeValueChangeListener(IValueChangeListener listener);}

IObservable 接口定义了侦听更改的一般方法。IObservableValue 接口通过添加特定值的概念以及显式地获取和设定该值的方法来对其加以扩展。

现在就定义好了一种编写代码的一般方法,以处理针对特定值的任何类型的更改。余下的工作就是适配引发更改的 Person bean 和文本小部件,使其适应此接口。

如何变魔术:Observable 工厂

粘贴到 BindingExample 类中的 createContext() 方法包含用于执行此适配过程的 API。JFace 数据绑定将让 observable 工厂系列的用户尝试将对象与查询到的 observable 匹配起来。如果工厂不匹配对象,则返回 null,然后数据绑定上下文将尝试列表中的下一个工厂。如果配置正确并且支持该对象类型,则返回一个适当的 observable。清单 5 显示了 SWTObservableFactory 中的一段代码,这段代码用于为许多常见的 SWT 控件生成 observable。

这段代码的 if 块中涉及的第一个问题是文本小部件。更新策略属性将确定 TextObservableValuec 是在发生更改(按下按键)时还是在焦点消失时提交更改。请注意,SWTObservableFactory 还支持其他常见的 SWT 小部件,例如标签、组合框、列表等。

清单 5. 构建 TextObservable 的工厂代码

if (description instanceof Text) {  int updatePolicy = new int[] {  SWT.Modify,  SWT.FocusOut,  SWT.None }[updateTime];  return new TextObservableValue/  ((Text) description, updatePolicy);} else if (description instanceof Button) {  // int updatePolicy = new int[] {  SWT.Modify,  SWT.FocusOut,  SWT.None }[updateTime];  return new ButtonObservableValue((Button) description);} else if (description instanceof Label) {  return new LabelObservableValue((Label) description);} else if (description instanceof Combo) {  return new ComboObservableList((Combo) description);} else if (description instanceof Spinner) {  return new SpinnerObservableValue((Spinner) description,   SWTProperties.SELECTION);} else if (description instanceof CCombo) {  return new CComboObservableList((CCombo) description);} else if (description instanceof List) {  return new ListObservableList((List) description);}

让我们来看看 TextObservableValue 类及其父类,如清单 6 所示,可以看到 getter 和 setter 方法最终都调用了适配文本小部件的方法。getter 和 setter 可以轻松地映射为 getText() 和 setText() 方法。此外,更新侦听程序将把焦点更改适配为一般更改事件。它将检查在创建 TextObservableValue 时指定的更新策略,并将本机文本小部件事件适配为一般的 IObservableValue 事件。

清单 6. TextObservableValue 的 get/set 方法

public final Object getValue() {  ...  return doGetValue();}public void setValue(Object value) {  Object currentValue = doGetValue();  ValueDiff diff = Diffs.createValueDiff(currentValue, value);  ...  doSetValue(value);  fireValueChange(diff);}public void doSetValue(final Object value) {  try {  updating = true;  bufferedValue = (String) value;  text.setText(value == null ? "" : value.toString()); //$NON-NLS-1$  } finally {  updating = false;  }}public Object doGetValue() {  return text.getText();}private Listener updateListener = new Listener() {  public void handleEvent(Event event) {  if (!updating) {   String ldValue = bufferedValue;   String newValue = text.getText();   // If we are updating on /   focus lost then when we fire the change   // event change the buffered value   if (updatePolicy == SWT.FocusOut) {   bufferedValue = text.getText();   if (!newValue.equals(oldValue)) {    fireValueChange/    (Diffs.createValueDiff(oldValue,     newValue));   }   } else {   fireValueChange/   (Diffs.createValueDiff(oldValue, text     .getText()));   }  }  }};

JFace 数据绑定还支持适配拥有 JavaBean 属性的标准对象,例如 Person bean。BeanObservableFactory 使用了 JavaBeanObservable 对象来适配特定属性,例如示例中的 first。

在修改示例时添加的 ctx.bind() 方法调用将使 observable 工厂得以运行。JFace 数据绑定 API 中的代码将获取目标对象和模型对象,并且将搜索适当的 observable 适配器。一旦找到用于绑定关系每一端的 observable 适配器,就会使用 ValueBinding() 类的实例将其绑定在一起。

如何变魔术:ValueBinding

为要绑定的两个实体创建了 observable 之后,需要一个第三方来使其保持同步。这个角色由 ValueBinding 类来承担;下面显示了一段简化的代码片段。

清单 7. ValueBinding 中的代码片段

public void updateModelFromTarget() {  updateModelFromTarget/  (Diffs.createValueDiff(target.getValue(), target   .getValue()));}public void updateModelFromTarget(ValueDiff diff) {  ...  model.setValue(target.getValue());   ...}

ValueBinding 的实例将侦听对目标和模型生成的 observable 所做的更改,并使用类似清单 7 所示的方法相应地同步更改。如您所见,updateModelFromTarget() 方法使用了由 IObservableValue 接口定义的一般访问方法来从目标中检索值并将其设定到模型中。

如何变魔术:整体来看

让我们回到在清单 3 中添加到 createControls() 方法中的 ctx.bind 代码行。每种绑定方法都以目标、模型和绑定规范作为实参(第 2 部分将提供更多关于绑定规范的详细信息)。

如果目标和模型实参都不能直接实现接口,则目标和模型最终都必须适配为 IObservable。在本例中,这项适配工作是由 IObservableFactory 来完成的。在使用 firstText 文本小部件的情况下,则不需要任何其他信息。当指定给 bind() 方法的目标/模型对象是文本小部件时,则认定与其文本属性的默认绑定应当已完成。

在使用 Person bean 的情况下,没有可绑定到的明显的默认属性。因此,Person bean 实例必须包装在一个 Property 对象中。此对象将用自己的 first 字符串添加足够的信息以使 BeanObservableFactory 可以确定要为 Person bean 中的哪一个属性创建 observable。

符合了所有这些规范后,bind() 方法最终将为指定的目标和模型创建 IObservable 适配器。然后该适配器将创建一个 ValueBinding 的实例,该实例使得在关系的一端发生更改时能够保持值的同步。

现在整个过程已经介绍完了,接下来看一看引入这些调用方法的顺序会很有帮助。清单 8 显示了一段堆栈跟踪,从焦点在文本小部件中消失时起,到由于 JFace 数据绑定从小部件中同步值而在 Person bean 中击中断点为止。请注意各种数据绑定和各种 JavaBeans 类 —— 您不必编写这些代码。

清单 8. 数据绑定同步的堆栈跟踪

Text(Control).setTabItemFocus() line: 2958Text(Control).forceFocus() line: 809Text(Control).sendFocusEvent(int, boolean) line: 2290Text(Widget).sendEvent(int) line: 1501Text(Widget).sendEvent(int, Event, boolean) line: 1520Text(Widget).sendEvent(Event) line: 1496EventTable.sendEvent(Event) line: 66TextObservableValue$1.handleEvent(Event) line: 51TextObservableValue.access$5(TextObservableValue, ValueDiff) line: 1TextObservableValue(AbstractObservableValue).fireValueChange(ValueDiff) line: 73ValueBinding$2.handleValueChange(IObservableValue, ValueDiff) line: 135ValueBinding.updateModelFromTarget(ValueDiff) line: 193JavaBeanObservableValue.setValue(Object) line: 88Method.invoke(Object, Object...) line: 585DelegatingMethodAccessorImpl.invoke(Object, Object[]) line: 25NativeMethodAccessorImpl.invoke(Object, Object[]) line: 39Person.setFirst(String) line: 17

图 10 是显示同步进程中各个角色之间如何关联的示意图。文本小部件和 Person bean 都被适配到 IObservableValue 接口。ValueBinding 类将侦听两端的适配器并使用这些适配器同步关系两端的更改。

图 10. observable 关系的示意图

从域对象启用更改

如果返回到 BindingExample,并查看清单 9 中的代码,将会注意到这段代码中仍有一个同步方法可以在 Person bean 中的值发生更改时更新 UI 控件。这是因为 Person Bean 在其值发生更改时不提供任何通知。通过允许 JFace 数据绑定来提供同步功能可以轻松地解决这个问题。

清单 9. 调用从 Person bean 到 UI 的同步的侦听程序

 button.addSelectionListener(new SelectionAdapter() {  public void /  widgetSelected(SelectionEvent e) {   updatePerson();   synchronizePersonToUI();  }  });  ...private void synchronizePersonToUI() {  this.firstText.setText(this.person.getFirst());  this.lastText.setText(this.person.getLast());}

修改 com.developerworks.basic.Person 类以扩展附带的 PropertyChangeAware 类。然后修改两个 setter 方法,如下所示。

清单 10. 将 PropertyChange 支持添加到 setter 中

public void setFirst(String first) {  Object ldVal = this.first;  this.first = first;  firePropertyChange("first", oldVal, this.first);}public String getLast() {  return last;}public void setLast(String last) {  Object ldVal = this.last;  this.last = last;  firePropertyChange("last", oldVal, this.last);}

PropertyChangeAware 类提供了标准的 JavaBean 属性更改支持。修改 setter 实现了当在 Person bean 中调用 setter 时对 PropertyChangeEvents 的触发功能。保存旧值,然后设定新值。最后,针对特定属性类型的两个值将触发一个属性更改事件。注意,使用的属性必须要遵循与 setter 方法相同的命名约定并且符合 JavaBean 规范。JFace 数据绑定提供的 JavaBeanObservable 可以侦听它所适配的对象的属性更改事件。这使它可以将特定的属性更改事件转换为更一般的 IObservableValue 更改事件。

这些更改完成后,删除 syncPersonToUI() 方法和在 Change Name 按钮侦听程序中对此方法的调用。同时,删除 createButtons() 方法中用于创建 Update Text From Person Bean 按钮的那段代码,因为不再需要此按钮。

启动 BindingExample 将显示类似图 11 所示的窗口。单击 Change Name 将导致文本小部件也发生变化。所有同步工作现在都由 JFace 数据绑定来完成。

图 11. 带属性更改支持的示例

由于您将不处理任何同步进程,因此也就不再需要两个私有的文本小部件变量了。createControls() 方法可被修改为使用本地变量,如清单 11 所示,因而完成初始示例的转换才能完全使用 JFace 数据绑定。

清单 11. 更改代码以使用本地变量

GridData gridData =  new GridData(GridData.FILL_HORIZONTAL);Text firstText = new Text(shell, SWT.BORDER);firstText.setLayoutData(gridData);label = new Label(shell, SWT.NONE);label.setText("Last:");Text lastText =  new Text(shell, SWT.BORDER);gridData =  new GridData(GridData.FILL_HORIZONTAL);lastText.setLayoutData(gridData);DataBindingContext ctx = createContext();ctx.bind(firstText,  new Property(this.person, "first"),  new BindSpec());ctx.bind(lastText,  new Property(this.person, "last"),  new BindSpec());

绑定其他控件和属性

文本控件不是惟一可绑定的 SWT 小部件。所有的标准 SWT 小部件,例如组合框和标签,都可用于绑定。您还可以绑定不可视的小部件属性,例如 enabled。将清单 12 中的代码复制到 Person Bean 中。

清单 12. 向 Person Bean 中添加对启用的支持

private boolean firstEnabled = true;public boolean getFirstEnabled() {  return firstEnabled;}public void setFirstEnabled(boolean firstEnabled) {  Object ldVal = this.firstEnabled;  this.firstEnabled = firstEnabled;  firePropertyChange("firstEnabled", /  oldVal, this.firstEnabled);}

现在修改示例中的 updatePerson() 方法。

清单 13. 修改 updatePerson() 方法

private void updatePerson() {  person.setFirst("James");  person.setLast("Gosling");  person.setFirstEnabled(false);}

最后,将下面所示的绑定添加到 createControls() 方法的末尾。

清单 14. 将标签和启用绑定起来

ctx.bind(new Property(firstText, "enabled"),  new Property(this.person, "firstEnabled"),  new BindSpec());ctx.bind(labelFirst,  new Property(this.person, "first"),  new BindSpec());ctx.bind(labelLast,  new Property(this.person, "last"),  new BindSpec());

新的绑定将导致示例的标签更改为与文本小部件相同的值。当单击 Change Name 时也会使第一个字段的小部件变为禁用状态。再次运行该示例,然后测试这项功能。

通过在 Last 字段中键入一些字符并按 Tab 键可以演示这些附加绑定的另一个有趣的结果。注意 Last 标签也发生了更改。JFace 数据绑定在焦点消失时将 Person Bean 的姓氏字段中的值与小部件进行了同步。由于标签被绑定到了此属性上,因此标签也被更新了。

绑定多个值

至此,您还只是将单个值绑定到小部件和小部件属性。在一个应用程序的 UI 中,很多时候都需要使用不止一个值。例如用户需要查看一组值,然后从中选择一个特定值。这通常是由列表或组合框来完成的。JFace 数据绑定考虑到了这种需求并提供了解决方案。

要创建一个绑定到多个值的示例,则需要一个要绑定的多个值的列表。此操作可通过将清单 15 中的代码复制到在本教程中不断增强的 Person Bean 中来完成。这段代码将创建一个名称的 ArrayList 以及相应的 getter。还有一种更简便的方法调用 —— addName() —— 该方法调用将获取 Person Bean 中的名字和姓氏,将名字和姓氏连接起来,然后把它们添加到列表中。

清单 15. 对 Person Bean 进行的修改

private List names;public Person(String first, String last) {     this.first = first;     this.last = last;     this.names = new ArrayList();     this.names.add("James Gosling");     this.names.add("Scott Delap");     this.names.add("Larry Wall");}...public List getAvailableNames() {     return this.names;}public void addName() {     this.names.add(getFirst() + " " + getLast());     firePropertyChange("availableNames", null, null);}

接下来,修改 BindingExample 类的代码,如清单 16 所示。将创建组合框和标签的代码以及绑定代码添加到 createControls() 方法中。然后在 createButtons() 方法中添加创建按钮的代码。

清单 16. 对 BindingExample 类进行的修改

private IObservableValue selectionHolder;private void createControls(Shell shell) {     ...     gridData = new GridData(GridData.FILL_HORIZONTAL);     gridData.horizontalSpan = 2;     Label comboSelectionLabel = new Label(shell, SWT.NONE);     comboSelectionLabel.setLayoutData(gridData);     gridData = new GridData(GridData.FILL_HORIZONTAL);     gridData.horizontalSpan = 2;     Combo combo = new Combo(shell, SWT.BORDER);     combo.setLayoutData(gridData);     DataBindingContext ctx = createContext();     ...     ctx.bind(        new Property(combo, SWTProperties.ITEMS),        new Property(            this.person,            "availableNames",            String.class,            true),        new BindSpec());     this.selectionHolder = new WritableValue(String.class);     ctx.bind(        new Property(            combo,            SWTProperties.SELECTION),            selectionHolder,            new BindSpec());     ctx.bind(comboSelectionLabel, selectionHolder, new BindSpec());}private void createButtons(Shell shell) {     ...     gridData = new GridData();     gridData.horizontalAlignment = SWT.CENTER;     gridData.horizontalSpan = 2;     button = new Button(shell, SWT.PUSH);     button.setLayoutData(gridData);     button.setText("Add Name");     button.addSelectionListener(new SelectionAdapter() {        public void widgetSelected(SelectionEvent e) {            person.addName();     selectionHolder.setValue(  person.getAvailableNames().get(  person.getAvailableNames().size() - 1));        }     });}

关于绑定组合框的第一点不同之处在于需要两个绑定。除了必须使用两个绑定来构成组合框外,这一点与先前示例中绑定文本小部件的 text 属性和 enabled 属性并不时完全不同。刚刚粘贴的代码将提供以下功能。

首先,创建一个组合框控件和一个标签。然后 Person Bean 中的可用名称列表被绑定到组合框控件上。由于此控件配有控件所含有的项列表及选项的属性,因此没有逻辑默认值可供 JFace 数据绑定提取以创建 observable —— 与先前的文本小部件不同(如果不指定显式属性,则文本小部件的默认值为 “text”)。因此,在绑定组合框时,必须显式指定属性。在使用第一个新绑定行的情况下,SWTProperties.ITEM 用来表示需要绑定可用项的列表。绑定列表时,Property 对象有一对附加参数。该对象的构造程序中的第三个参数将告诉绑定上下文在集合中找到了哪些类型的对象(第 2 部分将告诉您关于这一点为何重要的更多信息)。Property 构造程序中的第四个参数用于表示这是一个值的集合而不是作为对象的一个列表。

JFace 数据绑定要求为组合框的选项使用占位符。这个占位符可以为引用不在控件本身里的选项提供外部位置。这可以是 JavaBean 中的显式 getter/setter 属性,但通常是用于 UI(如本例)的一个单独的 holder。其结果是,创建了 WritableValue 实例,该实例用于实现 IObservableValue 以用作 holder。然后可以使用下一行中的 SWTProperties.SELECTION 常量将其绑定到组合框的 selection 属性。最后,为了向用户展示选项的绑定正在运行,同一个 WritableValue 实例也被绑定到标签上,该标签将随选项更改而更改。

对示例的另一处更改是添加了 Add Name 按钮。此按钮的选择侦听程序将调用 Person Bean 上的 addName() 方法,该方法将把当前的名称添加到可用名称列表中。然后将把新添加的值设为选项 holder 中的选项值。

运行示例将显示一个类似图 12 所示的窗口。从组合框中选择一个名称,标签将更改,因为标签被绑定到同一个选项 holder。接下来,在 First 和 Last 文本小部件中输入名称,然后单击 Add Name。系统将把该名称添加到组合框中,选中该名称,然后将其显示在标签中。

图 12. 修改后现在包括组合框的示例

列表里的奥秘:Observable

除了 IObservableValue 接口以外,JFace 数据绑定还定义了一个 IObservableList 接口,如清单 17 所示。正如 IObservableValue 将创建一种一般方法来侦听多个值的更改一样,IObservableList 也将指定一种一般方法来访问对象列表。执行赋值有一些标准方法,例如 contains()、add()、remove()、indexOf()、iterators() 等。

清单 17. IObservableList 接口

public interface IObservableList /extends List, IObservableCollection {  public void addListChangeListener/  (IListChangeListener listener);  public void removeListChangeListener/  (IListChangeListener listener);  public int size();  public boolean isEmpty();  public boolean contains(Object o);  public Iterator iterator();  public Object[] toArray();  public Object[] toArray(Object a[]);  public boolean add(Object o);  public boolean remove(Object o);  public boolean containsAll(Collection c);  public boolean addAll(Collection c);  public boolean addAll(int index, Collection c);  public boolean removeAll(Collection c);  public boolean retainAll(Collection c);  public boolean equals(Object o);  public int hashCode();  public Object get(int index);  public Object set(int index, Object element);  public Object remove(int index);  public int indexOf(Object o);  public int lastIndexOf(Object o);  public ListIterator listIterator();  public ListIterator listIterator(int index);  public List subList(int fromIndex, int toIndex);  Object getElementType();}

构建在 IObservableList 接口上的是 JavaBeanObservableList 实现,其中的代码片段如清单 18 所示。在大多数情况下,使用诸如 size() 之类的方法,上述实现将被委托给该实现所调整的列表。最重要的方法可能是 updateWrappedList()。此方法将获取一个旧版和一个新版列表,并将创建一个 Diff 对象。此对象将包含需要删除的项及需要添加的新项的更改记录。Diff 对象用于同步 IObservableList 的目标实现所需的更改。

清单 18. JavaBeanObservableList 中的代码片段

public int size() {    getterCalled();    return wrappedList.size();}protected void updateWrappedList(List newList) {    List ldList = wrappedList;    ListDiff listDiff = Diffs.computeListDiff(      oldList,   newList);    wrappedList = newList;    fireListChange(listDiff);}

清单 19 显示了 SWTObservableFactory 中的代码片段。可以看到 JFace 数据绑定包含了 ComboObservableList 和 ComboObservableValue 类以供生成组合框控件所需要的 observable 时使用。第一个类将把组合框的 items 属性适配为 IObservableList,第二个类将把 selection 属性适配为 IObservableValue 接口的 selection 属性。

清单 19. SWTObservableFactory 中的代码片段

if (object instanceof Combo     && (SWTProperties.TEXT.equals(attribute)|| SWTProperties.SELECTION.equals(attribute))) {  return new ComboObservableValue(  (Combo) object,         (String) attribute);} else if (object instanceof Combo     && SWTProperties.ITEMS.equals(attribute)) {     return new ComboObservableList((Combo) object);}

清单 20 显示来自各个类的代码片段。在使用 ComboObservableValue 组合框适配到 IObservable 值时,组合框既可以包含一个选项,也可以包含像文本小部件一样手动输入的文本,因此 get() 方法将检查这种情况并调用组合框控件上的相应值来检索值。类似地,setValue() 方法将检查哪个属性已被绑定并调用相应的 setter,然后触发一个值更改事件以通知感兴趣的各方。在大多数实例中,ComboObservableList 将委托给组合框。add() 和 remove() 等方法是例外,因为必须创建更改的 Diff 以供在 firstListChange() 方法中使用。

清单 20. ComboObservableValue 和 ComboObservableList 中的代码片段

public class ComboObservableValue extends AbstractObservableValue {...public void setValue(final Object value) {        String ldValue = combo.getText();        try {            updating = true;            if (attribute.equals(SWTProperties.TEXT)) {               String stringValue =               value != null ? value.toString() : ""; //$NON-NLS-1$               combo.setText(stringValue);            } else if (attribute.equals(SWTProperties.SELECTION)) {               String items[] = combo.getItems();               int index = -1;               if (items != null && value != null) {                   for (int i = 0; i < items.length; i++) {                      if (value.equals(items[i])) {                          index = i;                          break;                      }                   }                   if (index == -1) {                      combo.setText((String) value);                   } else {                      combo.select(index);                   }               }            }        } finally {            updating = false;        }        fireValueChange(Diffs.createValueDiff(oldValue, combo.getText()));     }     public Object doGetValue() {        if (attribute.equals(SWTProperties.TEXT))            return combo.getText();        Assert.isTrue(attribute.equals(SWTProperties.SELECTION),            "unexpected attribute: " + attribute); //$NON-NLS-1$        // The problem with a combo, is that it changes the text and         // fires before it update its selection index         return combo.getText();     }}public class ComboObservableList extends SWTObservableList {     private final Combo combo;...     public void add(int index, Object element) {        int size = doGetSize();        if (index  size)            index = size;        String[] newItems = new String[size + 1];        System.arraycopy(getItems(), 0, newItems, 0, index);        newItems[index] = (String) element;        System.arraycopy(   getItems(),   index,   newItems,   index + 1,   size - index);        setItems(newItems);        fireListChange(Diffs.createListDiff(Diffs.createListDiffEntry(index,               true, element)));     }     protected int getItemCount() {        return combo.getItemCount();     }     ...}

列表中的奥秘:ListBinding

选项 observable 是使用 ValueBinding 对象保持同步的,在前面已经详细介绍过了。

这样做导致了值列表仍需要同步。这项任务由 ListBinding 类来承担;清单 21 中显示了代码片段。

这个实现迭代任何指定的 difference,在目标 IObservableList 实例调用相应的 add() 或 remove() 方法。

清单 21. ListBinding 的代码片段

private IListChangeListener modelChangeListener =  new IListChangeListener() {            public void handleListChange(   IObservableList source,   ListDiff diff) {               ...               ListDiff setDiff = (ListDiff) e.diff;     ListDiffEntry[] differences =   setDiff.getDifferences();               for (int i = 0; i < differences.length; i++) {                   ListDiffEntry entry = differences[i];                   if (entry.isAddition()) {                      targetList.add(       entry.getPosition(),       entry.getElement());                   } else {                      targetList.remove(entry.getPosition());                   }               }            ...     }};

列表里的奥秘:作为整体来看

现在来总结一下,清单 16 中的 ctx.bind(new Property(combo, SWTProperties.ITEMS 代码行告诉绑定上下文将组合框的项属性绑定到在 Person Bean 上调用 availableNames getter 返回的值的 List。上下文将因存在绑定关系而为两者创建 IObservableList 实现。然后它将创建 ListBinding 引用 IObservableList 的实例,以使两者在一方发生更改时保持同步。

结束语

本教程介绍了 JFace 数据绑定 API 中的一些核心功能。同时,您也看到了数据绑定是如何将您从编写乏味的样本同步代码(桌面应用程序所必需)的痛苦中解脱出来的。JFace 数据绑定 API 提供了一组接口和实现以引用 JavaBean 的属性和 SWT/JFace 小部件的属性。

有了这种机制,它可以提供支持同步的小部件,例如文本控件和标签以及多值列表和组合框。您可以通过 DataBindingContext.bind()(提供关系的目标和模型)将这些属性绑定在一起。

本文配套源码

但是至少可以为自己的荷包省钱可以支些招,这点还是很现实的。

了解Eclipse中的JFace数据绑定,第2部分:绑定的基础知识

相关文章:

你感兴趣的文章:

标签云: