本教程基於 Eclipse 3.2 JFace 數據綁定 API,深入講解**轉換器**、**驗證器**、**表格主從綁定**及**Presentation Model 模式**構建可測試 UI 架構。前置需求:Java 經驗、SWT/JFace 基礎、已閱讀系列第一部分。
傳統桌面應用業務邏輯耦合 UI 控件,難以單元測試。**Presentation Model** 將狀態與業務邏輯從 UI 提取,測試無需實例化控件,UI 僅負責同步顯示。
class YearsMarriedEnablementListener implements ModifyListener {
public void modifyText(ModifyEvent e) {
boolean enable = nameTxt.getText().trim().length()>0
&& spouseTxt.getText().trim().length()>0;
if (!enable) yearsMarriedTxt.setText("");
yearsMarriedTxt.setEnabled(enable); // 直接操作 UI 控件
}
}
測試需啟動完整 UI,脆弱且緩慢。
Contact (name, spouse, yearsMarried)ContactPresentationModel 持有 Contact 與 enableYearsMarried 狀態,實現啟用邏輯,觸發 PropertyChangeEvent。DataBindingContext 綁定 UI 控件屬性與 Presentation Model 屬性。
// Presentation Model 核心邏輯
private class EnablementPropertyChangeListener implements PropertyChangeListener {
public void propertyChange(PropertyChangeEvent evt) {
boolean enable = contact.getName()!=null && !contact.getName().trim().isEmpty()
&& contact.getSpouse()!=null && !contact.getSpouse().trim().isEmpty();
if (!enable) contact.setYearsMarried(null);
setEnableYearsMarried(enable);
}
}
// 構造子註冊監聽
contact.addPropertyChangeListener("name", listener);
contact.addPropertyChangeListener("spouse", listener);
測試純單元,無 UI 依賴:
assertFalse(pm.getEnableYearsMarried());
pm.getContact().setName("Name"); assertFalse(pm.getEnableYearsMarried());
pm.getContact().setSpouse("Spouse"); assertTrue(pm.getEnableYearsMarried());
pm.getContact().setYearsMarried("5");
pm.getContact().setSpouse("");
assertFalse(pm.getEnableYearsMarried());
assertNull(pm.getContact().getYearsMarried());
BindSpec 指定模型↔目標雙向轉換器、驗證器:
IConverter:getFromType()、getToType()、convert(Object)IValidator:isPartiallyValid()(輸入中即時驗證)、isValid()(焦點離開完整驗證)需求:將「Married」核取方塊改為下拉框(Yes/No),綁定 boolean enableYearsMarried。
class BooleanToStringConverter implements IConverter {
Object getFromType() { return Boolean.TYPE; }
Object getToType() { return String.class; }
Object convert(Object v) { return ((Boolean)v) ? "Yes" : "No"; }
}
class StringToBooleanConverter implements IConverter { ... } // 反向
// 綁定下拉框
ctx.bind(new Property(combo, SWTProperties.SELECTION),
new Property(pm, "enableYearsMarried"),
new BindSpec(new BooleanToStringConverter(), new StringToBooleanConverter(), null, null));
class YearsMarriedValidator implements IValidator {
ValidationError isPartiallyValid(Object v) {
try { Integer.valueOf((String)v); return null; }
catch (NumberFormatException e) { return new ValidationError(ERROR, "Not A Number"); }
}
ValidationError isValid(Object v) {
return "5".equals(v) ? ValidationError.error("5 Is Not Allowed") : null;
}
}
// 綁定驗證器
ctx.bind(yearsMarriedTxt, new Property(contact, "yearsMarried"),
new BindSpec(null, null, new YearsMarriedValidator(), null));
Binding 物件暴露 getPartialValidationError()、getValidationError() 可觀測對象,綁定至 Label 顯示錯誤訊息:
ctx.bind(partialErrorLabel, binding.getPartialValidationError(),
new BindSpec(new ValidationErrorToStringConverter(), new ReadOnlyConverter(...), null, null));
ctx.bind(fullErrorLabel, binding.getValidationError(), ...);
List<Contact> contacts 與 WritableValue selectedContact(選中項佔位符)。TableViewer 綁定內容與單選:
ctx.bind(new Property(viewer, ViewersProperties.CONTENT),
new TableModelDescription(new Property(pm, "contacts", Contact.class, true),
new String[]{"name","spouse"}), null);
ctx.bind(new Property(viewer, ViewersProperties.SINGLE_SELECTION),
pm.getSelectedContactObservable(), null);
需註冊 ViewersObservableFactory、ViewersBindingFactory 至 DataBindingContext。
將 ContactForm 綁定至 IObservableValue(WritableValue)而非直接綁定 Contact:
// ContactForm 構造子接收 IObservableValue
ctx.bind(nameTxt, new Property(contactObs, "name", String.class, false), ...);
// 運行時注入選中項
ContactPresentationModel pm = new ContactPresentationModel(new WritableValue(Contact.class));
pm.getContactObservable().setValue(selectedContactFromTable);
表格選中項變更 → WritableValue 更新 → 詳情表單自動同步,雙向綁定無縫聯動。
JFace 數據綁定高階功能——轉換器、驗證器、主從表、間接綁定——配合 Presentation Model 模式,將開發者從繁瑣同步代碼中解脫,構建結構清晰、高度可測試、易維護的豐富客戶端應用。核心 API 涵蓋 JavaBean/SWT/JFace 屬性通用引用,強大擴展點支撐複雜業務場景。