開始之前
關於本系列
使用數據綁定 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 oldValue = 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: 2958
Text(Control).forceFocus() line: 809
Text(Control).sendFocusEvent(int, boolean) line: 2290
Text(Widget).sendEvent(int) line: 1501
Text(Widget).sendEvent(int, Event, boolean) line: 1520
Text(Widget).sendEvent(Event) line: 1496
EventTable.sendEvent(Event) line: 66
TextObservableValue$1.handleEvent(Event) line: 51
TextObservableValue.access$5(TextObservableValue, ValueDiff) line: 1
TextObservableValue(AbstractObservableValue).fireValueChange(ValueDiff) line: 73
ValueBinding$2.handleValueChange(IObservableValue, ValueDiff) line: 135
ValueBinding.updateModelFromTarget(ValueDiff) line: 193
JavaBeanObservableValue.setValue(Object) line: 88
Method.invoke(Object, Object...) line: 585
DelegatingMethodAccessorImpl.invoke(Object, Object[]) line: 25
NativeMethodAccessorImpl.invoke(Object, Object[]) line: 39
Person.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 oldVal = this.first;
this.first = first;
firePropertyChange("first", oldVal, this.first);
}
public String getLast() {
return last;
}
public void setLast(String last) {
Object oldVal = 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 oldVal = 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 oldList = 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 oldValue = 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 < 0 || 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()(提供關系的目標和模型)將這些屬性綁定在一起。
本文配套源碼