很多流行的 Web 應用程序都有視圖層的特性,視圖層足夠智能可以將請求和應答變量與 HTML 輸入標記同步。此過程可以輕松地完成,因為用戶輸入是通過 Web 應用程序的結構層和 HTTP 來路由的。而另一方面,Java GUI 應用程序經常都不能支持這種特性。無論是用 Standard Widget Toolkit (SWT) 編寫還是用 Swing 編寫,這些 Java GUI 應用程序的域對象與其 GUI 控件 (通常也稱為 組件) 之間通常都沒有定義好的路徑。
樣本(boilerplate)數據同步
打亂順序最糟糕的結果是造成數據混亂,最幸運的結果是大塊樣本同步代碼出現 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 數據綁定實現細節
介紹整個 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 字段變為組件模型本身。使用這種方法,當組件的視圖部分嘗試檢索或修改值時,它可直接轉到 Bean 中的值。JGoodies 數據綁定框架在很多情況下都使用了這種方法。
圖 1 顯示了 JGoodies 怎樣使用 DocumentAdapter 和 PropertyAdapter 類來裝飾 Bean 以將其用於 JTextComponent 的模型。
圖 1. JGoodies 調整字段用於 JTextComponent 模型
自動調用 getter 和 setter
將模型與 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();
}
}
與 Swing 進行數據綁定
現在有大量優秀的數據綁定框架可以與 Java GUI API 結合使用。下面是為與 Swing 應用程序結合使用創造出來的主要開源 API。
JGoodies 數據綁定 API
流行的 JGoodies 數據綁定 API 幾年前就以 Java.net 上的開源項目的形式出現了。它是由 Karsten Lentzsch 編寫的,他還是流行的 JGoodies FormLayout 的作者。此框架存在的歷史最久,已經經過多次修改和 bug 修正以使其用於產品用途時更加穩定。
Spring RCP
Spring Rich Client Platform (RCP) 項目 —— 流行的 Spring Application Framework 的子項目 —— 還包含一個 Swing 數據綁定框架。Spring RCP 和 JGoodies 都受到了 VisualWorks Smalltalk 中的數據綁定設計的影響。Spring RCP 還沒有完成 V1.0 發布。這說明,代碼庫的數據綁定部分十分穩定並且被很多開發人員使用著。
SwingLabs
Java.net 中的 SwingLabs 項目已經開展了多年的 Swing 數據綁定框架開發工作。不過,在此項目中取得的成果最近被轉到了一個由 Sun Microsystems 發起的新數據綁定 Java Specification Request (JSR) 上。
JSR 295:Bean 綁定
這是最近創建的 JSR 項目,由 Sun 的 Scott Violet 及專家組成員 Ben Galbraith 和 Karsten Lentzsch 共同領導,這個項目旨在為在桌面環境和服務器環境中使用的數據綁定提供標准的 API。JSR 295 尚處於開發階段,因此它不適於現在就需要這類解決方案的開發人員。
與 SWT 進行數據綁定
有兩個主要的開源數據綁定 API 用於與 SWT 結合使用。
SWTBinding
Jaysoft 接入了流行的 JGoodies 數據綁定 API 用於與 SWT 結合使用。核心類幾乎同 JGoodies 一樣。特定於 Swing 的模型則被適用於 SWT 控件的模型所替代。
JFace 數據綁定
最近出現的另一個 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 設計模式結合使用時能夠提高可測試性。